Skip to content

Conversation

sambsnyd
Copy link
Member

Gradle dependency declarations can take many forms. To list a subset of the variations:

dependencies {
        // Single-quoted and double quoted literal strings, with and without parentheses
    implementation 'com.google.guava:guava:29.0-jre'
    implementation "com.google.guava:guava:29.0-jre"
    compileOnly('com.google.guava:guava:29.0-jre')
    runtimeOnly("com.google.guava:guava:29.0-jre")


    // Map notation without parentheses
    implementation group: "com.google.guava", name: "guava", version: '29.0-jre'
    implementation(group: "com.google.guava", name: "guava", version: '29.0-jre')

    // interpolated
    def guavaVersion = '29.0-jre'
    implementation "com.google.guava:guava:$guavaVersion"
    implementation "com.google.guava:guava:${guavaVersion}"
    implementation group: "com.google.guava", name: "guava", version: guavaVersion
    implementation group: "com.google.guava", name: "guava", version: "$guavaVersion"
    implementation group: "com.google.guava", name: "guava", version: "${guavaVersion}"

    // Via project properties or ext properties
    implementation "com.google.guava:guava:${property('guavaVersion')}"
    implementation "com.google.guava:guava:${project.property('guavaVersion')}"n
    implementation "com.google.guava:guava:${project.findProperty('guavaVersion')}"
    implementation "com.google.guava:guava:${project.properties['guavaVersion']}"

    // Concatenation
    implementation 'com.google.guava:guava:' + guavaVersion

    // Multiple dependencies in single call (varargs, groovy only)
    implementation(
        'com.google.guava:guava-gwt:29.0-jre',
        'com.google.guava:guava:29.0-jre'
    )

    // platform() or enforcedPlatform() 
    implementation platform("com.google.guava:guava:29.0-jre")

    // version declared in platform/BOM, dependency substitution rule, etc.
    implementation("com.google.guava:guava")
}

The logic to handle these different syntax is scattered around the various dependency management recipes, which each of them covering incompletely-overlapping subsets.
This PR centralizes this syntactical knowledge into the GradleDependency trait and the GradleMultiDependency trait.

GradleDependency should now be a one-stop-shop for reading and refactoring dependency declarations of all kinds.
GradleMultiDependency exists only to abstract away the groovy-specific varargs version of a dependency declaration. This is necessary

Not every recipe has been updated to fully take advantage of this, but this PR is sprawling enough as-is.

Other enhancements include:

  • StringUtils.matchesGlob() no longer requires a null check to get the most commonly desired behavior
  • Remove need for org.openrewrite.gradle.internal.Dependency and its parser separate from org.openrewrite.maven.Dependency
  • Adoption of trait has slimmed a substantial number of lines from RemoveRedundantDependencyVersion and UpgradeDependencyVersion

sambsnyd and others added 16 commits October 15, 2025 12:19
- Refactored updateDependency() to accept GradleDependency parameter
- Added getConfigurationName() method to GradleDependency trait
- Fixed platform dependency handling in getPlatformDependency()
- Updated scanner to use trait's getConfigurationName() method
- Added varargs support attempt (not working yet)

Current status: 71/75 tests passing (94.7%)
Failing tests:
- varargsDependency
- leaveConstraintsAlone
- mapNotationKStringTemplateInterpolation (2 variants)
- Added scanVarargsDependencies() to properly scan all literal dependencies in varargs calls
- Added updateVarargsDependencies() to handle updating multiple dependencies in one call
- The GradleDependency trait only processes first argument, so varargs need special handling
- Varargs test now passes, but 3 other tests are failing and need investigation
- Created GradleMultiDependency trait to represent multiple dependencies in a single invocation
- Uses synthetic J.MethodInvocation wrappers for each dependency
- Provides map() method for clean functional transformation of dependencies
- Refactored UpgradeDependencyVersion to use the new trait
- Removed ad-hoc varargs handling code in favor of trait-based approach
- Varargs test still passes, maintaining same 3 unrelated failing tests
- GradleMultiDependency now tracks groupId/artifactId filters from its Matcher
- Added map(DependencyMatcher, mapper) method for explicit filtering
- The trait only transforms dependencies that match the configured filters
- This ensures selective updates when multiple dependencies are in one invocation
- Added comprehensive tests to verify filtering behavior
- Prevents unintended changes to non-matching dependencies in varargs
- Add support for K.StringTemplate in map notation (GradleDependency.getVersionVariable)
  Fixes mapNotationKStringTemplateInterpolation test

- Skip upgrading dependencies without versions unless they are platform dependencies
  Prevents upgrading constraint-governed dependencies
  Fixes leaveConstraintsAlone test

All 75 UpgradeDependencyVersionTest tests passing

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
- Remove redundant regex check in UpgradeDependencyVersion scanning phase
  GradleDependency.getVersionVariable() already returns null for literal versions,
  so checking !declaredVersion.matches("^[\\d\\.].*") was unnecessary

- Clean up scanDependency to use trait methods directly (getGroupId/getArtifactId)
  instead of getDeclaredGroupId/getDeclaredArtifactId

- Simplify updateDependency to check declaredVersion == null instead of
  checking isPlatform() for constraint handling

All 75 UpgradeDependencyVersionTest tests still passing

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
…andler.

Incidentally resolves an OOM I was getting running locally due to ClasspathScanningLoader. There has to be something we can do to not have classgraph use all the memory.
@sambsnyd sambsnyd force-pushed the dependency-refactoring branch from cc760b3 to 069755b Compare October 15, 2025 19:19
J.MethodInvocation m = super.visitMethodInvocation(method, ctx);

Optional<GradleDependency> maybeGradleDependency = new GradleDependency.Matcher()
m = GradleMultiDependency.matcher()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
m = GradleMultiDependency.matcher()
return GradleMultiDependency.matcher()

Comment on lines +48 to +49
import java.util.Optional;
import java.util.stream.Collectors;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
import java.util.Optional;
import java.util.stream.Collectors;
import java.util.Optional;

Comment on lines +770 to +772
@Nullable
private static String extractPropertyNameFromMethodInvocation(J.MethodInvocation mi) {
if (mi.getSimpleName().equals("property") || mi.getSimpleName().equals("findProperty")) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
@Nullable
private static String extractPropertyNameFromMethodInvocation(J.MethodInvocation mi) {
if (mi.getSimpleName().equals("property") || mi.getSimpleName().equals("findProperty")) {
private static @Nullable String extractPropertyNameFromMethodInvocation(J.MethodInvocation mi) {
if ("property".equals(mi.getSimpleName()) || "findProperty".equals(mi.getSimpleName())) {

)
);
}
} No newline at end of file
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
}
}

* Helper method to extract a string value from various expression types.
* Handles literals, identifiers, field access, method invocations, and GStrings.
*/
private @Nullable String extractValueAsString(Expression value) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm a little worried about the prints contained in here for a couple of reasons:

  • Sustainability/Incorrect results
  • If a marker exists on one of these elements from a previous recipe, then the marker information will end up being printed here

Comment on lines +1328 to +1331
GradleDependencyConfiguration gdc = getConfiguration(gradleProject, methodInvocation);
if (gdc == null && !(DEPENDENCY_DSL_MATCHER.matches(methodInvocation) && !"project".equals(methodInvocation.getSimpleName()))) {
return null;
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

callout: We're already performing this same check within isDependencyDeclaration above.

@github-project-automation github-project-automation bot moved this from In Progress to Ready to Review in OpenRewrite Oct 15, 2025
sambsnyd and others added 2 commits October 15, 2025 12:29
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
J.MethodInvocation m = super.visitMethodInvocation(method, ctx);

Optional<GradleDependency> maybeGradleDependency = new GradleDependency.Matcher()
m = GradleMultiDependency.matcher()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
m = GradleMultiDependency.matcher()
return GradleMultiDependency.matcher()

Comment on lines +48 to +49
import java.util.Optional;
import java.util.stream.Collectors;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
import java.util.Optional;
import java.util.stream.Collectors;
import java.util.Optional;

Comment on lines +770 to +772
@Nullable
private static String extractPropertyNameFromMethodInvocation(J.MethodInvocation mi) {
if (mi.getSimpleName().equals("property") || mi.getSimpleName().equals("findProperty")) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
@Nullable
private static String extractPropertyNameFromMethodInvocation(J.MethodInvocation mi) {
if (mi.getSimpleName().equals("property") || mi.getSimpleName().equals("findProperty")) {
private static @Nullable String extractPropertyNameFromMethodInvocation(J.MethodInvocation mi) {
if ("property".equals(mi.getSimpleName()) || "findProperty".equals(mi.getSimpleName())) {

Comment on lines +787 to +788
@Nullable
private static String extractPropertyNameFromGBinary(G.Binary binary) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
@Nullable
private static String extractPropertyNameFromGBinary(G.Binary binary) {
private static @Nullable String extractPropertyNameFromGBinary(G.Binary binary) {

((G.MapLiteral) firstArg).getElements() : m.getArguments().stream()
.filter(G.MapEntry.class::isInstance)
.map(G.MapEntry.class::cast)
.collect(Collectors.toList());
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
.collect(Collectors.toList());
.collect(toList());

((G.MapLiteral) firstArg).getElements() : m.getArguments().stream()
.filter(G.MapEntry.class::isInstance)
.map(G.MapEntry.class::cast)
.collect(Collectors.toList());
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
.collect(Collectors.toList());
.collect(toList());

m.getArguments().stream()
.filter(G.MapEntry.class::isInstance)
.map(G.MapEntry.class::cast)
.collect(Collectors.toList());
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
.collect(Collectors.toList());
.collect(toList());

)
);
}
} No newline at end of file
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
}
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Ready to Review

Development

Successfully merging this pull request may close these issues.

2 participants