Skip to content

Commit

Permalink
Find relocated dependencies (#59)
Browse files Browse the repository at this point in the history
* Parse old groupIds from `jonathanlermitage/oga-maven-plugin`

* Add license to parser

* Split in parser already; read in scanning recipe

* Ensure artifactIds are null when they need to be

* Add visitor implementation for Maven dependencies and plugins

* Initial support for Groovy

* Complete dependency support Gradle

* Credit `oga-maven-plugin` authors in description

* Add a minimal test for the parser

* Add missing license headers

* Use `TreeVisitor.noop()` in scan phase

* Also bump plugin dependencies

* Drop org.jetbrains.annotations.NotNull
  • Loading branch information
timtebeek authored Jan 20, 2024
1 parent 20f82dc commit 3a41ece
Show file tree
Hide file tree
Showing 10 changed files with 1,335 additions and 0 deletions.
53 changes: 53 additions & 0 deletions .github/workflows/migrations.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
---
name: Update the Old GroupId migrations CSV

on:
workflow_dispatch: {}
schedule:
- cron: 0 11 * * WED

jobs:
update-migrations:
runs-on: ubuntu-latest
steps:
# Checkout and build parser
- name: Checkout
uses: actions/checkout@v4
- uses: actions/setup-java@v4
with:
cache: 'gradle'
distribution: 'temurin'
java-version: '17'
- name: Setup Gradle
uses: gradle/gradle-build-action@v2
- name: Run build with Gradle Wrapper
run: ./gradlew build deps

# Update migrations
- name: Checkout oga-maven-plugin
uses: actions/checkout@v4
with:
repository: jonathanlermitage/oga-maven-plugin
path: oga-maven-plugin
- name: Update migrations
run: java -classpath "build/deps/*:build/libs/*" org.openrewrite.java.dependencies.oldgroupids.ParseDefinitionMigrations oga-maven-plugin src/main/resources/migrations.csv

# Create pull request
- name: Timestamp
run: echo "NOW=$(date +'%Y-%m-%dT%H%M')" >> $GITHUB_ENV
- name: Create Pull Request
id: cpr
uses: peter-evans/create-pull-request@v5
with:
base: main
branch: migrations/${{ env.NOW }}
title: "[Auto] Old GroupId migrations as of ${{ env.NOW }}"
body: |
[Auto] Old GroupId migrations as of ${{ env.NOW }}.
commit-message: "[Auto] Old GroupId migrations as of ${{ env.NOW }}"
labels: enhancement
- name: Check outputs
if: ${{ steps.cpr.outputs.pull-request-number }}
run: |
echo "Pull Request Number - ${{ steps.cpr.outputs.pull-request-number }}"
echo "Pull Request URL - ${{ steps.cpr.outputs.pull-request-url }}"
4 changes: 4 additions & 0 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -55,3 +55,7 @@ tasks {
into("build/deps")
}
}

license {
exclude("**/*.json")
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,252 @@
/*
* Copyright 2024 the original author or authors.
* <p>
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* <p>
* https://www.apache.org/licenses/LICENSE-2.0
* <p>
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.openrewrite.java.dependencies;

import com.fasterxml.jackson.databind.MappingIterator;
import com.fasterxml.jackson.dataformat.csv.CsvMapper;
import lombok.Value;
import org.openrewrite.*;
import org.openrewrite.groovy.GroovyIsoVisitor;
import org.openrewrite.groovy.tree.G;
import org.openrewrite.internal.StringUtils;
import org.openrewrite.internal.lang.Nullable;
import org.openrewrite.java.MethodMatcher;
import org.openrewrite.java.dependencies.oldgroupids.Migration;
import org.openrewrite.java.tree.Expression;
import org.openrewrite.java.tree.J;
import org.openrewrite.marker.SearchResult;
import org.openrewrite.maven.MavenIsoVisitor;
import org.openrewrite.xml.XPathMatcher;
import org.openrewrite.xml.tree.Xml;

import java.io.IOException;
import java.io.UncheckedIOException;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;

public class RelocatedDependencyCheck extends ScanningRecipe<RelocatedDependencyCheck.Accumulator> {
@Override
public String getDisplayName() {
return "Find relocated dependencies";
}

@Override
public String getDescription() {
//language=markdown
return "Find Maven and Gradle dependencies and Maven plugins that have relocated to a new `groupId` or `artifactId`. " +
"Relocation information comes from the [oga-maven-plugin](https://github.com/jonathanlermitage/oga-maven-plugin/) " +
"maintained by Jonathan Lermitage, Filipe Roque and others.";
}

@Value
public static class Accumulator {
Map<GroupArtifact, Relocation> migrations;
}

@Value
static class GroupArtifact {
String groupId;
@Nullable
String artifactId;
}

@Value
static class Relocation {
GroupArtifact to;
String context;
}

@Override
public Accumulator getInitialValue(ExecutionContext ctx) {
try {
MappingIterator<Migration> objectMappingIterator = new CsvMapper()
.readerWithSchemaFor(Migration.class)
.readValues(RelocatedDependencyCheck.class.getResourceAsStream("/migrations.csv"));
Map<GroupArtifact, Relocation> migrations = new HashMap<>();
while (objectMappingIterator.hasNext()) {
Migration def = objectMappingIterator.next();
GroupArtifact oldGav = new GroupArtifact(def.getOldGroupId(), StringUtils.isBlank(def.getOldArtifactId()) ? null : def.getOldArtifactId());
GroupArtifact newGav = new GroupArtifact(def.getNewGroupId(), StringUtils.isBlank(def.getNewArtifactId()) ? null : def.getNewArtifactId());
migrations.put(oldGav, new Relocation(newGav, StringUtils.isBlank(def.getContext()) ? null : def.getContext()));
}
return new Accumulator(migrations);
} catch (IOException e) {
throw new UncheckedIOException(e);
}
}

@Override
public TreeVisitor<?, ExecutionContext> getScanner(Accumulator acc) {
return TreeVisitor.noop();
}

@Override
public TreeVisitor<?, ExecutionContext> getVisitor(Accumulator acc) {
return new TreeVisitor<Tree, ExecutionContext>() {
private final TreeVisitor<?, ExecutionContext> gradleVisitor = gradleVisitor(acc);
private final TreeVisitor<?, ExecutionContext> mavenVisitor = mavenVisitor(acc);

@Override
public @Nullable Tree visit(@Nullable Tree tree, ExecutionContext ctx) {

if (!(tree instanceof SourceFile)) {
return tree;
}
SourceFile s = (SourceFile) tree;
if (gradleVisitor.isAcceptable(s, ctx)) {
s = (SourceFile) gradleVisitor.visitNonNull(s, ctx);
} else if (mavenVisitor.isAcceptable(s, ctx)) {
s = (SourceFile) mavenVisitor.visitNonNull(s, ctx);
}
return s;
}
};
}

private TreeVisitor<?, ExecutionContext> gradleVisitor(Accumulator acc) {
MethodMatcher dependencyMatcher = new MethodMatcher("DependencyHandlerSpec *(..)");
return new GroovyIsoVisitor<ExecutionContext>() {
@Override
public J.MethodInvocation visitMethodInvocation(J.MethodInvocation method, ExecutionContext ctx) {
J.MethodInvocation mi = super.visitMethodInvocation(method, ctx);
if (dependencyMatcher.matches(mi)) {
List<Expression> methodArguments = mi.getArguments();
Expression firstMethodArgument = methodArguments.get(0);
if (firstMethodArgument instanceof J.Literal) {
J.Literal literal = (J.Literal) firstMethodArgument;
mi = searchInLiteral(literal, mi);
} else if (firstMethodArgument instanceof G.GString) {
G.GString gString = (G.GString) firstMethodArgument;
List<J> strings = gString.getStrings();
if (!strings.isEmpty() && strings.get(0) instanceof J.Literal) {
mi = searchInLiteral((J.Literal) strings.get(0), mi);
}
} else if (firstMethodArgument instanceof G.MapEntry) {
mi = searchInGMapEntry(methodArguments, mi);
}

}
return mi;
}

private J.MethodInvocation searchInLiteral(J.Literal literal, J.MethodInvocation mi) {
String gav = (String) literal.getValue();
assert gav != null;
String[] parts = gav.split(":");
if (gav.length() >= 2) {
mi = maybeAddComment(acc, mi, parts[0], parts[1]);
}
return mi;
}

private J.MethodInvocation searchInGMapEntry(List<Expression> methodArguments, J.MethodInvocation mi) {
String groupId = null;
String artifactId = null;
for (Expression e : methodArguments) {
if (!(e instanceof G.MapEntry)) {
continue;
}
G.MapEntry arg = (G.MapEntry) e;
if (!(arg.getKey() instanceof J.Literal)) {
continue;
}
J.Literal key = (J.Literal) arg.getKey();
Expression argValue = arg.getValue();
String valueValue = null;
if (argValue instanceof J.Literal) {
J.Literal value = (J.Literal) argValue;
if (value.getValue() instanceof String) {
valueValue = (String) value.getValue();
}
} else if (argValue instanceof J.Identifier) {
J.Identifier value = (J.Identifier) argValue;
valueValue = value.getSimpleName();
} else if (argValue instanceof G.GString) {
G.GString value = (G.GString) argValue;
List<J> strings = value.getStrings();
if (!strings.isEmpty() && strings.get(0) instanceof G.GString.Value) {
G.GString.Value versionGStringValue = (G.GString.Value) strings.get(0);
if (versionGStringValue.getTree() instanceof J.Identifier) {
valueValue = ((J.Identifier) versionGStringValue.getTree()).getSimpleName();
}
}
}
if (!(key.getValue() instanceof String)) {
continue;
}
String keyValue = (String) key.getValue();
if ("group".equals(keyValue)) {
groupId = valueValue;
} else if ("name".equals(keyValue)) {
artifactId = valueValue;
}
}
if (groupId != null) {
mi = maybeAddComment(acc, mi, groupId, artifactId);
}
return mi;
}

};
}

private static TreeVisitor<?, ExecutionContext> mavenVisitor(Accumulator acc) {
final XPathMatcher dependencyMatcher = new XPathMatcher("//dependencies/dependency");
return new MavenIsoVisitor<ExecutionContext>() {
@Override
public Xml.Tag visitTag(Xml.Tag tag, ExecutionContext ctx) {
tag = super.visitTag(tag, ctx);
Optional<String> optionalGroupId = tag.getChildValue("groupId");
Optional<String> optionalArtifactId = tag.getChildValue("artifactId");
if (dependencyMatcher.matches(getCursor())) {
if (optionalGroupId.isPresent()) {
String groupId = optionalGroupId.get();
String artifactId = optionalArtifactId.orElse(null);
tag = maybeAddComment(acc, tag, groupId, artifactId);
}
} else if (isPluginTag()) {
if (optionalArtifactId.isPresent()) {
String groupId = tag.getChildValue("groupId").orElse("org.apache.maven.plugins");
String artifactId = optionalArtifactId.get();
tag = maybeAddComment(acc, tag, groupId, artifactId);
}
}
return tag;
}

};
}

private static <T extends Tree> T maybeAddComment(Accumulator acc, T tree, String groupId, @Nullable String artifactId) {
Relocation relocation = acc.getMigrations().get(new GroupArtifact(groupId, artifactId));
if (relocation != null) {
String commentText = String.format("Relocated to %s%s%s",
relocation.getTo().getGroupId(),
Optional.ofNullable(relocation.getTo().getArtifactId()).map(a -> ":" + a).orElse(""),
relocation.getContext() == null ? "" : " as per \"" + relocation.getContext() + "\"");
return SearchResult.found(tree, commentText);
}
if (artifactId == null) {
return tree;
}
// Try again without artifactId
return maybeAddComment(acc, tree, groupId, null);
}
}


Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
/*
* Copyright 2024 the original author or authors.
* <p>
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* <p>
* https://www.apache.org/licenses/LICENSE-2.0
* <p>
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.openrewrite.java.dependencies.oldgroupids;

import com.fasterxml.jackson.annotation.JsonPropertyOrder;
import lombok.AllArgsConstructor;
import lombok.Data;
import org.openrewrite.internal.lang.Nullable;

@Data
@AllArgsConstructor
@JsonPropertyOrder({"oldGroupId", "oldArtifactId", "newGroupId", "newArtifactId", "context"})
public class Migration {
String oldGroupId;
@Nullable
String oldArtifactId;
String newGroupId;
@Nullable
String newArtifactId;
@Nullable
String context;
}
Loading

0 comments on commit 3a41ece

Please sign in to comment.