diff --git a/rewrite-java-test/src/test/java/org/openrewrite/java/AddNullMethodArgumentTest.java b/rewrite-java-test/src/test/java/org/openrewrite/java/AddNullMethodArgumentTest.java
new file mode 100644
index 00000000000..686a9a25a2d
--- /dev/null
+++ b/rewrite-java-test/src/test/java/org/openrewrite/java/AddNullMethodArgumentTest.java
@@ -0,0 +1,89 @@
+/*
+ * Copyright 2024 the original author or authors.
+ *
+ * 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
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * 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;
+
+import org.junit.jupiter.api.Test;
+import org.openrewrite.test.RecipeSpec;
+import org.openrewrite.test.RewriteTest;
+
+import static org.openrewrite.java.Assertions.java;
+
+class AddNullMethodArgumentTest implements RewriteTest {
+
+ @Override
+ public void defaults(RecipeSpec spec) {
+ spec.parser(JavaParser.fromJavaVersion().dependsOn("""
+ class B {
+ static void foo() {}
+ static void foo(Integer n) {}
+ static void foo(Integer n1, Integer n2) {}
+ static void foo(Integer n1, Integer n2, Integer n3) {}
+ static void foo(Integer n1, Integer n2, Integer n3, Integer n4) {}
+ static void foo(Integer n1, Integer n2, Integer n3, String n4) {}
+ B() {}
+ B(Integer n) {}
+ }
+ """));
+ }
+
+ @Test
+ void addToMiddleArgument() {
+ rewriteRun(
+ spec -> spec.recipe(new AddNullMethodArgument("B foo(Integer, Integer)", 1, "java.lang.Integer", "n2", false)),
+ java(
+ "class A {{ B.foo(0, 1); }}",
+ "class A {{ B.foo(0, null, 1); }}"
+ )
+ );
+ }
+
+ @Test
+ void addArgumentsConsecutively() {
+ rewriteRun(
+ spec -> spec.recipes(
+ new AddNullMethodArgument("B foo(Integer)", 1, "java.lang.Integer", "n2", false),
+ new AddNullMethodArgument("B foo(Integer, Integer)", 1, "java.lang.Integer", "n2", false)
+ ),
+ java(
+ "class A {{ B.foo(0); }}",
+ "class A {{ B.foo(0, null, null); }}"
+ )
+ );
+ }
+
+ @Test
+ void addToConstructorArgument() {
+ rewriteRun(
+ spec -> spec.recipe(new AddNullMethodArgument("B ()", 0, "java.lang.Integer", "arg", false)),
+ java(
+ "class A { B b = new B(); }",
+ "class A { B b = new B(null); }"
+ )
+ );
+ }
+
+ @Test
+ void addCastToConflictingArgumentType() {
+ rewriteRun(
+ spec -> spec.recipe(new AddNullMethodArgument("B foo(Integer,Integer,Integer)", 3, "java.lang.String", "n2", true)),
+ java(
+ "class A {{ B.foo(0, 1, 2); }}",
+ "class A {{ B.foo(0, 1, 2, (java.lang.String) null); }}"
+ )
+ );
+ }
+
+}
diff --git a/rewrite-java-test/src/test/java/org/openrewrite/java/JavaParserTest.java b/rewrite-java-test/src/test/java/org/openrewrite/java/JavaParserTest.java
index df35ab67a72..931d06254ff 100644
--- a/rewrite-java-test/src/test/java/org/openrewrite/java/JavaParserTest.java
+++ b/rewrite-java-test/src/test/java/org/openrewrite/java/JavaParserTest.java
@@ -20,6 +20,8 @@
import org.intellij.lang.annotations.Language;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
import org.openrewrite.InMemoryExecutionContext;
import org.openrewrite.Issue;
import org.openrewrite.SourceFile;
@@ -130,4 +132,19 @@ public void methodB() {}
)));
}
}
+
+ @ParameterizedTest
+ // language=java
+ @ValueSource(strings = {
+ "package my.example; class PrivateClass { void foo() {} } public class PublicClass { void bar() {} }",
+ "package my.example; public class PublicClass { void bar() {} } class PrivateClass { void foo() {} }"
+ })
+ void shouldResolvePathUsingPublicClasses(@Language("java") String source) {
+ rewriteRun(
+ java(
+ source,
+ spec -> spec.afterRecipe(cu -> assertThat(cu.getSourcePath()).isEqualTo(Path.of("my","example","PublicClass.java")))
+ )
+ );
+ }
}
diff --git a/rewrite-java/src/main/java/org/openrewrite/java/AddNullMethodArgument.java b/rewrite-java/src/main/java/org/openrewrite/java/AddNullMethodArgument.java
new file mode 100644
index 00000000000..745373d7ba6
--- /dev/null
+++ b/rewrite-java/src/main/java/org/openrewrite/java/AddNullMethodArgument.java
@@ -0,0 +1,149 @@
+/*
+ * Copyright 2024 the original author or authors.
+ *
+ * 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
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * 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;
+
+import lombok.EqualsAndHashCode;
+import lombok.Value;
+import org.openrewrite.*;
+import org.openrewrite.internal.ListUtils;
+import org.openrewrite.internal.lang.Nullable;
+import org.openrewrite.java.search.UsesMethod;
+import org.openrewrite.java.tree.*;
+import org.openrewrite.marker.Markers;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import static org.openrewrite.Tree.randomId;
+import static org.openrewrite.java.tree.Space.EMPTY;
+
+/**
+ * This recipe finds method invocations matching a method pattern and uses a zero-based argument index to determine
+ * which argument is added with a null value.
+ */
+@Value
+@EqualsAndHashCode(callSuper = false)
+public class AddNullMethodArgument extends Recipe {
+
+ /**
+ * A method pattern that is used to find matching method invocations.
+ * See {@link MethodMatcher} for details on the expression's syntax.
+ */
+ @Option(displayName = "Method pattern",
+ description = "A method pattern that is used to find matching method invocations.",
+ example = "com.yourorg.A foo(int, int)")
+ String methodPattern;
+
+ /**
+ * A zero-based index that indicates which argument will be added as null to the method invocation.
+ */
+ @Option(displayName = "Argument index",
+ description = "A zero-based index that indicates which argument will be added as null to the method invocation.",
+ example = "0")
+ int argumentIndex;
+
+ @Option(displayName = "Parameter type",
+ description = "The type of the parameter that we add the argument for.",
+ example = "java.lang.String")
+ String parameterType;
+
+ @Option(displayName = "Parameter name",
+ description = "The name of the parameter that we add the argument for.",
+ required = false,
+ example = "name")
+ @Nullable String parameterName;
+
+ @Option(displayName = "Explicit cast",
+ description = "Explicitly cast the argument to the parameter type. Useful if the method is overridden with another type.",
+ required = false,
+ example = "true")
+ @Nullable Boolean explicitCast;
+
+ @Override
+ public String getInstanceNameSuffix() {
+ return String.format("%d in methods `%s`", argumentIndex, methodPattern);
+ }
+
+ @Override
+ public String getDisplayName() {
+ return "Add a `null` method argument";
+ }
+
+ @Override
+ public String getDescription() {
+ return "Add a `null` argument to method invocations.";
+ }
+
+ @Override
+ public TreeVisitor, ExecutionContext> getVisitor() {
+ return Preconditions.check(new UsesMethod<>(methodPattern), new AddNullMethodArgumentVisitor(new MethodMatcher(methodPattern)));
+ }
+
+ private class AddNullMethodArgumentVisitor extends JavaIsoVisitor {
+ private final MethodMatcher methodMatcher;
+
+ public AddNullMethodArgumentVisitor(MethodMatcher methodMatcher) {
+ this.methodMatcher = methodMatcher;
+ }
+
+ @Override
+ public J.MethodInvocation visitMethodInvocation(J.MethodInvocation method, ExecutionContext ctx) {
+ J.MethodInvocation m = super.visitMethodInvocation(method, ctx);
+ return (J.MethodInvocation) visitMethodCall(m);
+ }
+
+ @Override
+ public J.NewClass visitNewClass(J.NewClass newClass, ExecutionContext ctx) {
+ J.NewClass n = super.visitNewClass(newClass, ctx);
+ return (J.NewClass) visitMethodCall(n);
+ }
+
+ private MethodCall visitMethodCall(MethodCall methodCall) {
+ MethodCall m = methodCall;
+ List originalArgs = m.getArguments();
+ if (methodMatcher.matches(m) && (long) originalArgs.size() >= argumentIndex) {
+ List args = new ArrayList<>(originalArgs);
+
+ if (args.size() == 1 && args.get(0) instanceof J.Empty) {
+ args.remove(0);
+ }
+
+ Expression nullLiteral = new J.Literal(randomId(), args.isEmpty() ? Space.EMPTY : Space.SINGLE_SPACE, Markers.EMPTY, "null", "null", null, JavaType.Primitive.Null);
+ if (explicitCast == Boolean.TRUE) {
+ nullLiteral = new J.TypeCast(randomId(), Space.SINGLE_SPACE, Markers.EMPTY,
+ new J.ControlParentheses<>(randomId(), EMPTY, Markers.EMPTY,
+ new JRightPadded<>(TypeTree.build(parameterType), EMPTY, Markers.EMPTY)),
+ nullLiteral);
+ }
+ m = m.withArguments(ListUtils.insert(args, nullLiteral, argumentIndex));
+
+ JavaType.Method methodType = m.getMethodType();
+ if (methodType != null) {
+ m = m.withMethodType(methodType
+ .withParameterNames(ListUtils.insert(methodType.getParameterNames(),
+ parameterName == null ? "arg" + argumentIndex : parameterName, argumentIndex))
+ .withParameterTypes(ListUtils.insert(methodType.getParameterTypes(),
+ JavaType.buildType(parameterType), argumentIndex)));
+ if (m instanceof J.MethodInvocation && ((J.MethodInvocation) m).getName().getType() != null) {
+ m = ((J.MethodInvocation) m).withName(((J.MethodInvocation) m).getName().withType(m.getMethodType()));
+ }
+ }
+ }
+ return m;
+ }
+
+ }
+}
diff --git a/rewrite-java/src/main/java/org/openrewrite/java/JavaParser.java b/rewrite-java/src/main/java/org/openrewrite/java/JavaParser.java
index f7d63f65300..491c784d4c2 100644
--- a/rewrite-java/src/main/java/org/openrewrite/java/JavaParser.java
+++ b/rewrite-java/src/main/java/org/openrewrite/java/JavaParser.java
@@ -423,9 +423,14 @@ default Path sourcePathFromSourceText(Path prefix, String sourceCode) {
static Path resolveSourcePathFromSourceText(Path prefix, String sourceCode) {
Pattern packagePattern = Pattern.compile("^package\\s+([^;]+);");
Pattern classPattern = Pattern.compile("(class|interface|enum|record)\\s*(<[^>]*>)?\\s+(\\w+)");
+ Pattern publicClassPattern = Pattern.compile("public\\s+" + classPattern.pattern());
Function simpleName = sourceStr -> {
- Matcher classMatcher = classPattern.matcher(sourceStr);
+ Matcher classMatcher = publicClassPattern.matcher(sourceStr);
+ if (classMatcher.find()) {
+ return classMatcher.group(3);
+ }
+ classMatcher = classPattern.matcher(sourceStr);
return classMatcher.find() ? classMatcher.group(3) : null;
};
diff --git a/rewrite-maven/src/main/java/org/openrewrite/maven/UpgradePluginVersion.java b/rewrite-maven/src/main/java/org/openrewrite/maven/UpgradePluginVersion.java
index cbbd89c12cb..19c0316bbf9 100755
--- a/rewrite-maven/src/main/java/org/openrewrite/maven/UpgradePluginVersion.java
+++ b/rewrite-maven/src/main/java/org/openrewrite/maven/UpgradePluginVersion.java
@@ -19,6 +19,7 @@
import lombok.Value;
import org.openrewrite.*;
import org.openrewrite.internal.lang.Nullable;
+import org.openrewrite.marker.ci.GithubActionsBuildEnvironment;
import org.openrewrite.maven.search.FindPlugin;
import org.openrewrite.maven.table.MavenMetadataFailures;
import org.openrewrite.maven.tree.ResolvedPom;
@@ -164,7 +165,7 @@ private Optional findNewerDependencyVersion(String groupId, String artif
}
@Value
- @EqualsAndHashCode(callSuper = true)
+ @EqualsAndHashCode(callSuper = false)
private static class ChangePluginVersionVisitor extends MavenVisitor {
String groupId;
String artifactId;
diff --git a/rewrite-test/src/main/java/org/openrewrite/Issue.java b/rewrite-test/src/main/java/org/openrewrite/Issue.java
index b17f7db557b..de56e03ea66 100644
--- a/rewrite-test/src/main/java/org/openrewrite/Issue.java
+++ b/rewrite-test/src/main/java/org/openrewrite/Issue.java
@@ -16,6 +16,7 @@
package org.openrewrite;
import java.lang.annotation.ElementType;
+import java.lang.annotation.Repeatable;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@@ -25,6 +26,13 @@
*/
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.SOURCE)
+@Repeatable(Issue.Issues.class)
public @interface Issue {
String value();
+
+ @Target({ElementType.TYPE, ElementType.METHOD})
+ @Retention(RetentionPolicy.SOURCE)
+ @interface Issues {
+ Issue[] value();
+ }
}