Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Find relocated dependencies #59

Merged
merged 13 commits into from
Jan 20, 2024
Merged
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