diff --git a/src/main/java/org/openrewrite/java/migrate/ArrayStoreExceptionToTypeNotPresentException.java b/src/main/java/org/openrewrite/java/migrate/ArrayStoreExceptionToTypeNotPresentException.java index bbf1d8653c..3c79e15b75 100644 --- a/src/main/java/org/openrewrite/java/migrate/ArrayStoreExceptionToTypeNotPresentException.java +++ b/src/main/java/org/openrewrite/java/migrate/ArrayStoreExceptionToTypeNotPresentException.java @@ -16,47 +16,449 @@ package org.openrewrite.java.migrate; import lombok.Getter; +import org.jspecify.annotations.Nullable; +import org.openrewrite.Cursor; import org.openrewrite.ExecutionContext; import org.openrewrite.Preconditions; -import org.openrewrite.Recipe; +import org.openrewrite.ScanningRecipe; +import org.openrewrite.Tree; import org.openrewrite.TreeVisitor; import org.openrewrite.internal.ListUtils; -import org.openrewrite.java.ChangeType; import org.openrewrite.java.JavaIsoVisitor; -import org.openrewrite.java.search.FindMethods; +import org.openrewrite.java.MethodMatcher; +import org.openrewrite.java.marker.JavaProject; import org.openrewrite.java.search.UsesMethod; -import org.openrewrite.java.tree.J; -import org.openrewrite.java.tree.TypeUtils; +import org.openrewrite.java.tree.*; +import org.openrewrite.marker.Markers; -public class ArrayStoreExceptionToTypeNotPresentException extends Recipe { +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.concurrent.atomic.AtomicBoolean; + +import static java.util.Arrays.asList; +import static java.util.Collections.emptyList; + +public class ArrayStoreExceptionToTypeNotPresentException extends ScanningRecipe { private static final String ARRAY_STORE_EXCEPTION = "java.lang.ArrayStoreException"; private static final String TYPE_NOT_PRESENT_EXCEPTION = "java.lang.TypeNotPresentException"; + private static final String TYPE_NOT_PRESENT_EXCEPTION_SIMPLE_NAME = "TypeNotPresentException"; + private static final String PACKAGE_SEPARATOR = "."; + private static final String NESTED_TYPE_SEPARATOR = "$"; + private static final MethodMatcher CLASS_GET_ANNOTATION = new MethodMatcher("java.lang.Class getAnnotation(java.lang.Class)"); + + /** + * A catch of any of these types already handles {@code TypeNotPresentException}. + */ + private static final Set HANDLES_TYPE_NOT_PRESENT_EXCEPTION = new HashSet<>(asList( + "java.lang.RuntimeException", "java.lang.Exception", "java.lang.Throwable")); + + /** + * The supertypes of {@code RuntimeException}, a closed set since {@code java.lang} cannot be extended. A + * position declared with one of these accepts every {@code RuntimeException}, so it survives the widening. + * Unresolved types are absent and so block it. + */ + private static final Set SUPERTYPES_OF_RUNTIME_EXCEPTION = new HashSet<>(asList( + "java.lang.RuntimeException", "java.lang.Exception", "java.lang.Throwable", + "java.lang.Object", "java.io.Serializable")); @Getter final String displayName = "Catch `TypeNotPresentException` thrown by `Class.getAnnotation()`"; @Getter - final String description = "Replace catch blocks for `ArrayStoreException` around `Class.getAnnotation()` with `TypeNotPresentException` to ensure compatibility with Java 11+."; + final String description = "Also catch `TypeNotPresentException` where `ArrayStoreException` is caught around `Class.getAnnotation()` to ensure compatibility with Java 11+. " + + "The `ArrayStoreException` is retained as the protected code can still throw it for reasons unrelated to annotations."; + + /** + * Where the sources declare their own {@code TypeNotPresentException}, the spliced simple name could + * resolve to it instead, either failing to compile or silently catching the wrong type, so the scanner + * records those declarations and the visitor leaves the affected sources unchanged. Scoped per + * {@link JavaProject} marker, since only a same-module declaration can shadow; unmarked sources share + * one scope. + */ + public static class Accumulator { + private final Set<@Nullable JavaProject> declaringProjects = new HashSet<>(); + + void recordDeclaration(@Nullable JavaProject project) { + declaringProjects.add(project); + } + + boolean declaresTypeNotPresentException(@Nullable JavaProject project) { + return declaringProjects.contains(project); + } + } + + @Override + public Accumulator getInitialValue(ExecutionContext ctx) { + return new Accumulator(); + } @Override - public TreeVisitor getVisitor() { - String classGetAnnotationPattern = "java.lang.Class getAnnotation(java.lang.Class)"; - return Preconditions.check(new UsesMethod<>(classGetAnnotationPattern), new JavaIsoVisitor() { + public TreeVisitor getScanner(Accumulator acc) { + return new JavaIsoVisitor() { + @Override + public J.ClassDeclaration visitClassDeclaration(J.ClassDeclaration classDecl, ExecutionContext ctx) { + if (TYPE_NOT_PRESENT_EXCEPTION_SIMPLE_NAME.equals(classDecl.getSimpleName())) { + acc.recordDeclaration(javaProject(getCursor().firstEnclosing(JavaSourceFile.class))); + } + return super.visitClassDeclaration(classDecl, ctx); + } + }; + } + + @Override + public TreeVisitor getVisitor(Accumulator acc) { + return Preconditions.check(new UsesMethod<>(CLASS_GET_ANNOTATION), new JavaIsoVisitor() { @Override public J.Try visitTry(J.Try tryStatement, ExecutionContext ctx) { J.Try try_ = super.visitTry(tryStatement, ctx); - if (FindMethods.find(try_, classGetAnnotationPattern).isEmpty()) { + JavaSourceFile sourceFile = getCursor().firstEnclosing(JavaSourceFile.class); + if (!(sourceFile instanceof J.CompilationUnit)) { + // A multi-catch is Java-only syntax, so other JVM languages are left alone return try_; } + if (anyCatchConcernsTypeNotPresentException(try_) || !protectedRegionCallsGetAnnotation(try_) || + anyEnclosingCatchConcernsTypeNotPresentException(getCursor()) || + typeNotPresentExceptionSimpleNameIsShadowed((J.CompilationUnit) sourceFile, acc, javaProject(sourceFile))) { + return try_; + } + Cursor tryCursor = getCursor(); return try_.withCatches(ListUtils.map(try_.getCatches(), catch_ -> { - if (TypeUtils.isOfClassType(catch_.getParameter().getType(), ARRAY_STORE_EXCEPTION)) { - return (J.Try.Catch) new ChangeType(ARRAY_STORE_EXCEPTION, TYPE_NOT_PRESENT_EXCEPTION, true) - .getVisitor().visit(catch_, ctx); + if (TypeUtils.isOfClassType(catch_.getParameter().getType(), ARRAY_STORE_EXCEPTION) && + allParameterReferencesSurviveWidening(catch_, tryCursor)) { + return alsoCatchTypeNotPresentException(catch_); } return catch_; })); } }); } + + /** + * Only a try's resources and body are protected by its catches. A catch or finally block runs outside that + * region, as do the method bodies of a lambda or class created inside it. Such a class's instance + * initializers do run inside, but are left out too, which only costs a migration that is not applied. + */ + private static boolean protectedRegionCallsGetAnnotation(J.Try try_) { + AtomicBoolean found = new AtomicBoolean(false); + JavaIsoVisitor scanner = new JavaIsoVisitor() { + @Override + public J.MethodInvocation visitMethodInvocation(J.MethodInvocation method, AtomicBoolean found) { + if (CLASS_GET_ANNOTATION.matches(method)) { + found.set(true); + return method; + } + return super.visitMethodInvocation(method, found); + } + + @Override + public J.Lambda visitLambda(J.Lambda lambda, AtomicBoolean found) { + return lambda; + } + + @Override + public J.ClassDeclaration visitClassDeclaration(J.ClassDeclaration classDecl, AtomicBoolean found) { + return classDecl; + } + + @Override + public J.NewClass visitNewClass(J.NewClass newClass, AtomicBoolean found) { + if (newClass.getBody() != null) { + // Constructor arguments and instance initializers are evaluated here, only method bodies are + // deferred; the whole body is left out anyway, costing only a migration that is not applied + for (Expression argument : newClass.getArguments()) { + visit(argument, found); + } + return newClass; + } + return super.visitNewClass(newClass, found); + } + }; + if (try_.getResources() != null) { + for (J.Try.Resource resource : try_.getResources()) { + scanner.visit(resource, found); + } + } + scanner.visit(try_.getBody(), found); + return found.get(); + } + + private static boolean anyCatchConcernsTypeNotPresentException(J.Try try_) { + for (J.Try.Catch catch_ : try_.getCatches()) { + TypeTree typeExpression = catch_.getParameter().getTree().getTypeExpression(); + if (typeExpression instanceof J.MultiCatch) { + for (NameTree alternative : ((J.MultiCatch) typeExpression).getAlternatives()) { + if (concernsTypeNotPresentException(alternative.getType())) { + return true; + } + } + } else if (typeExpression != null && concernsTypeNotPresentException(typeExpression.getType())) { + return true; + } + } + return false; + } + + /** + * An enclosing try whose protected region contains this one sees every {@code TypeNotPresentException} this + * try does not catch, so widening here would silently reroute them and any such enclosing try blocks it. + * Only enclosing tries containing this one in their body or resources count. The walk does not stop at + * lambda or class boundaries, whose bodies may run inside the enclosing region, erring towards not widening. + */ + private static boolean anyEnclosingCatchConcernsTypeNotPresentException(Cursor tryCursor) { + J child = tryCursor.getValue(); + for (Cursor cursor = tryCursor.getParent(); cursor != null; cursor = cursor.getParent()) { + Object value = cursor.getValue(); + if (value instanceof J.Try) { + J.Try enclosing = (J.Try) value; + boolean inProtectedRegion = child == enclosing.getBody() || + enclosing.getResources() != null && enclosing.getResources().contains(child); + if (inProtectedRegion && anyCatchConcernsTypeNotPresentException(enclosing)) { + return true; + } + } + if (value instanceof J) { + child = (J) value; + } + } + return false; + } + + /** + * A supertype catch already handles it, and a catch of it or a subclass would become unreachable. + */ + private static boolean concernsTypeNotPresentException(@Nullable JavaType type) { + JavaType.FullyQualified fullyQualified = TypeUtils.asFullyQualified(type); + return fullyQualified != null && + (HANDLES_TYPE_NOT_PRESENT_EXCEPTION.contains(fullyQualified.getFullyQualifiedName()) || + TypeUtils.isAssignableTo(TYPE_NOT_PRESENT_EXCEPTION, fullyQualified)); + } + + /** + * Per JLS 14.20 a multi-catch parameter is implicitly final and typed as the least upper bound of the + * alternatives, here {@code RuntimeException}. Rather than prove every way a handler can depend on the + * narrower type safe, only a short allow list of common contexts is recognized; anything else leaves the + * catch untouched, which only costs a migration that is not applied. + */ + private static boolean allParameterReferencesSurviveWidening(J.Try.Catch catch_, Cursor tryCursor) { + List variables = catch_.getParameter().getTree().getVariables(); + if (variables.size() != 1) { + return false; + } + String parameterName = variables.get(0).getSimpleName(); + JavaType.Variable parameterType = variables.get(0).getVariableType(); + AtomicBoolean unsafe = new AtomicBoolean(false); + new JavaIsoVisitor() { + @Override + public J.Identifier visitIdentifier(J.Identifier identifier, AtomicBoolean unsafe) { + if (referencesParameter(identifier, getCursor(), parameterName, parameterType) && + !widenedReferenceIsSafe(getCursor())) { + unsafe.set(true); + } + return identifier; + } + }.visit(catch_, unsafe, tryCursor); + return !unsafe.get(); + } + + /** + * Whether this identifier uses the catch parameter. Provably-something-else identifiers, such as method or + * member names, declarations and labels, are skipped; an unattributed one counts as a use. + */ + private static boolean referencesParameter(J.Identifier identifier, Cursor cursor, String parameterName, + JavaType.@Nullable Variable parameterType) { + if (!parameterName.equals(identifier.getSimpleName())) { + return false; + } + J parent = cursor.getParentTreeCursor().getValue(); + if (parent instanceof J.MethodInvocation && ((J.MethodInvocation) parent).getName() == identifier || + parent instanceof J.FieldAccess && ((J.FieldAccess) parent).getName() == identifier || + parent instanceof J.MemberReference && ((J.MemberReference) parent).getReference() == identifier || + parent instanceof J.VariableDeclarations.NamedVariable && ((J.VariableDeclarations.NamedVariable) parent).getName() == identifier || + parent instanceof J.Label || parent instanceof J.Break || parent instanceof J.Continue) { + return false; + } + JavaType.Variable fieldType = identifier.getFieldType(); + if (fieldType != null && parameterType != null) { + return fieldType == parameterType || + fieldType.getName().equals(parameterType.getName()) && + TypeUtils.isOfType(fieldType.getType(), parameterType.getType()); + } + return true; + } + + /** + * Whether an expression the widening retypes keeps compiling, and keeps its meaning, in its context. A + * deliberately short allow list covering the common handler shapes; anything unrecognized blocks the + * widening. + */ + private static boolean widenedReferenceIsSafe(Cursor cursor) { + J expression = cursor.getValue(); + Cursor parentCursor = cursor.getParentTreeCursor(); + J parent = parentCursor.getValue(); + if (parent instanceof J.Parentheses) { + // The parenthesized expression widens with its content + return widenedReferenceIsSafe(parentCursor); + } + if (parent instanceof J.Binary || parent instanceof J.InstanceOf || parent instanceof J.Throw) { + // Concatenation, `==`/`!=`, a type test and throwing (`RuntimeException` is unchecked) all stay + // valid and unchanged for the values the original handler could receive + return true; + } + if (parent instanceof J.MethodInvocation) { + J.MethodInvocation invocation = (J.MethodInvocation) parent; + if (expression == invocation.getSelect()) { + // Per JLS 4.3.2 `e.getClass()` is `Class` over the receiver's *static* type, so + // its result widens with the receiver and blocks the widening + return invokedMethodRemainsAvailable(invocation.getMethodType()) && + !"getClass".equals(invocation.getSimpleName()); + } + return argumentRemainsCompatible(invocation.getMethodType(), invocation.getArguments().indexOf(expression)); + } + if (parent instanceof J.NewClass) { + return argumentRemainsCompatible(((J.NewClass) parent).getMethodType(), + ((J.NewClass) parent).getArguments().indexOf(expression)); + } + if (parent instanceof J.VariableDeclarations.NamedVariable) { + // Covers an explicit declared type; `var` infers the narrower type and is rejected here + J.VariableDeclarations.NamedVariable variable = (J.VariableDeclarations.NamedVariable) parent; + return expression == variable.getInitializer() && acceptsAnyRuntimeException(variable.getType()); + } + if (parent instanceof J.Assignment) { + // The parameter as the assigned variable fails: a multi-catch parameter is implicitly final + J.Assignment assignment = (J.Assignment) parent; + return expression != assignment.getVariable() && acceptsAnyRuntimeException(assignment.getVariable().getType()); + } + return false; + } + + /** + * A method stays available when its declaring type is a supertype of {@code RuntimeException}, which holds + * for every resolvable call since {@code ArrayStoreException} declares none of its own. + */ + private static boolean invokedMethodRemainsAvailable(JavaType.@Nullable Method methodType) { + return methodType != null && acceptsAnyRuntimeException(methodType.getDeclaringType()); + } + + private static boolean argumentRemainsCompatible(JavaType.@Nullable Method methodType, int argumentIndex) { + if (methodType == null || argumentIndex < 0) { + // Unresolved: the parameter's requirements are unknowable, so leave the catch alone + return false; + } + // The resolved type reports the *inferred* argument type, so a generic parameter such as + // ` T requireNonNull(T)` resolves to `ArrayStoreException` and is rejected with it + JavaType parameterType = parameterType(methodType, argumentIndex); + return parameterType != null && acceptsAnyRuntimeException(parameterType); + } + + private static @Nullable JavaType parameterType(JavaType.Method methodType, int argumentIndex) { + List parameterTypes = methodType.getParameterTypes(); + if (parameterTypes.isEmpty()) { + return null; + } + int parameterIndex = Math.min(argumentIndex, parameterTypes.size() - 1); + JavaType parameterType = parameterTypes.get(parameterIndex); + if (methodType.hasFlags(Flag.Varargs) && parameterIndex == parameterTypes.size() - 1 && + parameterType instanceof JavaType.Array) { + // The reference is never an array, so in the variable arity position it is passed as an element + return ((JavaType.Array) parameterType).getElemType(); + } + return parameterType; + } + + private static boolean acceptsAnyRuntimeException(@Nullable JavaType type) { + JavaType.FullyQualified fullyQualified = TypeUtils.asFullyQualified(type); + return fullyQualified != null && SUPERTYPES_OF_RUNTIME_EXCEPTION.contains(fullyQualified.getFullyQualifiedName()); + } + + /** + * Whether the simple name could resolve to anything but {@code java.lang.TypeNotPresentException}: a class + * of that name declared anywhere in this file's {@link JavaProject}, a type parameter of that name in this + * file, a single-type import of another such class, or any such type already referenced here. Such sources + * are left unchanged; emitting a qualified name instead was judged not worth the machinery. A shadowing + * class visible only as a compiled dependency, or declared in another project, cannot be seen from here + * and is not detected. + */ + private static boolean typeNotPresentExceptionSimpleNameIsShadowed(J.CompilationUnit cu, Accumulator acc, + @Nullable JavaProject project) { + if (acc.declaresTypeNotPresentException(project)) { + return true; + } + for (J.Import import_ : cu.getImports()) { + if (TYPE_NOT_PRESENT_EXCEPTION_SIMPLE_NAME.equals(import_.getQualid().getSimpleName())) { + JavaType.FullyQualified imported = TypeUtils.asFullyQualified(import_.getQualid().getType()); + if (imported == null || !TYPE_NOT_PRESENT_EXCEPTION.equals(imported.getFullyQualifiedName())) { + return true; + } + } + } + for (JavaType type : cu.getTypesInUse().getTypesInUse()) { + JavaType.FullyQualified used = TypeUtils.asFullyQualified(type); + if (used != null && isForeignTypeNotPresentException(used.getFullyQualifiedName())) { + return true; + } + } + return declaresTypeNotPresentException(cu); + } + + /** + * The {@link JavaProject} marker of this source file, or null where the build attached none, as in a + * single-module parse; unmarked sources share the null scope. + */ + private static @Nullable JavaProject javaProject(@Nullable JavaSourceFile sourceFile) { + return sourceFile == null ? null : sourceFile.getMarkers().findFirst(JavaProject.class).orElse(null); + } + + private static boolean isForeignTypeNotPresentException(String fullyQualifiedName) { + return !TYPE_NOT_PRESENT_EXCEPTION.equals(fullyQualifiedName) && + (TYPE_NOT_PRESENT_EXCEPTION_SIMPLE_NAME.equals(fullyQualifiedName) || + fullyQualifiedName.endsWith(PACKAGE_SEPARATOR + TYPE_NOT_PRESENT_EXCEPTION_SIMPLE_NAME) || + fullyQualifiedName.endsWith(NESTED_TYPE_SEPARATOR + TYPE_NOT_PRESENT_EXCEPTION_SIMPLE_NAME)); + } + + private static boolean declaresTypeNotPresentException(J.CompilationUnit cu) { + AtomicBoolean found = new AtomicBoolean(false); + new JavaIsoVisitor() { + @Override + public J.ClassDeclaration visitClassDeclaration(J.ClassDeclaration classDecl, AtomicBoolean found) { + if (TYPE_NOT_PRESENT_EXCEPTION_SIMPLE_NAME.equals(classDecl.getSimpleName())) { + found.set(true); + return classDecl; + } + return super.visitClassDeclaration(classDecl, found); + } + + @Override + public J.TypeParameter visitTypeParameter(J.TypeParameter typeParameter, AtomicBoolean found) { + if (typeParameter.getName() instanceof J.Identifier && + TYPE_NOT_PRESENT_EXCEPTION_SIMPLE_NAME.equals(((J.Identifier) typeParameter.getName()).getSimpleName())) { + found.set(true); + return typeParameter; + } + return super.visitTypeParameter(typeParameter, found); + } + }.visit(cu, found); + return found.get(); + } + + /** + * Assembled directly rather than through {@code JavaTemplate}: a catch parameter is not an insertion point, + * and regenerating the catch would discard the type expression as written along with the parameter's + * modifiers and annotations. Keeping it as the first alternative preserves all of that, as + * {@code CombineSemanticallyEqualCatchBlocks} does upstream. + */ + private static J.Try.Catch alsoCatchTypeNotPresentException(J.Try.Catch catch_) { + J.VariableDeclarations parameter = catch_.getParameter().getTree(); + TypeTree typeExpression = parameter.getTypeExpression(); + if (typeExpression == null) { + return catch_; + } + TypeTree typeNotPresentException = new J.Identifier(Tree.randomId(), Space.SINGLE_SPACE, Markers.EMPTY, + emptyList(), TYPE_NOT_PRESENT_EXCEPTION_SIMPLE_NAME, JavaType.ShallowClass.build(TYPE_NOT_PRESENT_EXCEPTION), null); + J.MultiCatch multiCatch = new J.MultiCatch(Tree.randomId(), typeExpression.getPrefix(), Markers.EMPTY, asList( + JRightPadded.build(typeExpression.withPrefix(Space.EMPTY)).withAfter(Space.SINGLE_SPACE), + JRightPadded.build(typeNotPresentException))); + return catch_.withParameter(catch_.getParameter().withTree(parameter.withTypeExpression(multiCatch))); + } } diff --git a/src/main/resources/META-INF/rewrite/examples.yml b/src/main/resources/META-INF/rewrite/examples.yml index 1dfc38ed48..6c3c527436 100644 --- a/src/main/resources/META-INF/rewrite/examples.yml +++ b/src/main/resources/META-INF/rewrite/examples.yml @@ -347,7 +347,7 @@ examples: type: specs.openrewrite.org/v1beta/example recipeName: org.openrewrite.java.migrate.ArrayStoreExceptionToTypeNotPresentException examples: -- description: '`ArrayStoreExceptionToTypeNotPresentExceptionTest#replaceCaughtException`' +- description: '`ArrayStoreExceptionToTypeNotPresentExceptionTest#alsoCatchTypeNotPresentException`' sources: - before: | import java.lang.annotation.*; @@ -377,12 +377,12 @@ examples: try { Object o = "test"; o.getClass().getAnnotation(Override.class); - } catch (TypeNotPresentException e) { + } catch (ArrayStoreException | TypeNotPresentException e) { System.out.println("Caught Exception"); } try { Object.class.getAnnotation(Override.class); - } catch (TypeNotPresentException e) { + } catch (ArrayStoreException | TypeNotPresentException e) { System.out.println("Caught ArrayStoreException"); } } diff --git a/src/main/resources/META-INF/rewrite/recipes.csv b/src/main/resources/META-INF/rewrite/recipes.csv index e51cb061b2..f76e523f07 100644 --- a/src/main/resources/META-INF/rewrite/recipes.csv +++ b/src/main/resources/META-INF/rewrite/recipes.csv @@ -18,7 +18,7 @@ maven,org.openrewrite.recipe:rewrite-migrate-java,org.openrewrite.java.migrate.A maven,org.openrewrite.recipe:rewrite-migrate-java,org.openrewrite.java.migrate.AddStaticVariableOnProducerSessionBean,Adds `static` modifier to `@Produces` fields that are in session beans,"Ensures that the fields annotated with `@Produces` which is inside the session bean (`@Stateless`, `@Stateful`, or `@Singleton`) are declared `static`.",1,,,Modernize,Java,,,Modernize your code to best use the project's current JDK version. Take advantage of newly available APIs and reduce the dependency of your code on third party dependencies where there is equivalent functionality in the Java standard library.,Basic building blocks for transforming Java code.,, maven,org.openrewrite.recipe:rewrite-migrate-java,org.openrewrite.java.migrate.AddSuppressionForIllegalReflectionWarningsPlugin,Add maven jar plugin to suppress illegal reflection warnings,Adds a maven jar plugin that's configured to suppress Illegal Reflection Warnings.,1,,,Modernize,Java,,,Modernize your code to best use the project's current JDK version. Take advantage of newly available APIs and reduce the dependency of your code on third party dependencies where there is equivalent functionality in the Java standard library.,Basic building blocks for transforming Java code.,"[{""name"":""version"",""type"":""String"",""displayName"":""Version"",""description"":""An exact version number, or node-style semver selector used to select the version number."",""example"":""29.X""}]", maven,org.openrewrite.recipe:rewrite-migrate-java,org.openrewrite.java.migrate.AddSurefireFailsafeArgLine,Add `argLine` to surefire and failsafe plugins,"Adds the specified arguments to the `argLine` configuration of the Maven Surefire and Failsafe plugins, merging with any existing argLine value without duplicating arguments. The `@{argLine}` [late property reference](https://maven.apache.org/surefire/maven-surefire-plugin/faq.html) is prepended so that an agent injected by another plugin during the build, such as the JaCoCo coverage agent from `jacoco-maven-plugin:prepare-agent`, is preserved rather than overwritten. It is not added when the existing `argLine` already references the `argLine` property.",1,,,Modernize,Java,,,Modernize your code to best use the project's current JDK version. Take advantage of newly available APIs and reduce the dependency of your code on third party dependencies where there is equivalent functionality in the Java standard library.,Basic building blocks for transforming Java code.,"[{""name"":""argLine"",""type"":""String"",""displayName"":""Arg line"",""description"":""The arguments to add to the surefire and failsafe plugin `argLine` configuration. Individual arguments are space-separated. Arguments already present in the existing argLine are not duplicated."",""example"":""--add-opens java.base/java.lang=ALL-UNNAMED --add-opens java.base/java.util=ALL-UNNAMED"",""required"":true}]", -maven,org.openrewrite.recipe:rewrite-migrate-java,org.openrewrite.java.migrate.ArrayStoreExceptionToTypeNotPresentException,Catch `TypeNotPresentException` thrown by `Class.getAnnotation()`,Replace catch blocks for `ArrayStoreException` around `Class.getAnnotation()` with `TypeNotPresentException` to ensure compatibility with Java 11+.,1,,,Modernize,Java,,,Modernize your code to best use the project's current JDK version. Take advantage of newly available APIs and reduce the dependency of your code on third party dependencies where there is equivalent functionality in the Java standard library.,Basic building blocks for transforming Java code.,, +maven,org.openrewrite.recipe:rewrite-migrate-java,org.openrewrite.java.migrate.ArrayStoreExceptionToTypeNotPresentException,Catch `TypeNotPresentException` thrown by `Class.getAnnotation()`,Also catch `TypeNotPresentException` where `ArrayStoreException` is caught around `Class.getAnnotation()` to ensure compatibility with Java 11+. The `ArrayStoreException` is retained as the protected code can still throw it for reasons unrelated to annotations.,1,,,Modernize,Java,,,Modernize your code to best use the project's current JDK version. Take advantage of newly available APIs and reduce the dependency of your code on third party dependencies where there is equivalent functionality in the Java standard library.,Basic building blocks for transforming Java code.,, maven,org.openrewrite.recipe:rewrite-migrate-java,org.openrewrite.java.migrate.BeanDiscovery,Behavior change to bean discovery in modules with `beans.xml` file with no version specified,Alters beans with missing version attribute to include this attribute as well as the bean-discovery-mode="all" attribute to maintain an explicit bean archive.,1,,,Modernize,Java,,,Modernize your code to best use the project's current JDK version. Take advantage of newly available APIs and reduce the dependency of your code on third party dependencies where there is equivalent functionality in the Java standard library.,Basic building blocks for transforming Java code.,, maven,org.openrewrite.recipe:rewrite-migrate-java,org.openrewrite.java.migrate.BeansXmlNamespace,Change `beans.xml` `schemaLocation` to match XML namespace,Set the `schemaLocation` that corresponds to the `xmlns` set in `beans.xml` files.,1,,,Modernize,Java,,,Modernize your code to best use the project's current JDK version. Take advantage of newly available APIs and reduce the dependency of your code on third party dependencies where there is equivalent functionality in the Java standard library.,Basic building blocks for transforming Java code.,, maven,org.openrewrite.recipe:rewrite-migrate-java,org.openrewrite.java.migrate.BounceCastleFromJdk15OntoJdk18On,Migrate Bouncy Castle to `jdk18on`,This recipe will upgrade Bouncy Castle dependencies from `-jdk15on` or `-jdk15to18` to `-jdk18on`.,15,,,Modernize,Java,,,Modernize your code to best use the project's current JDK version. Take advantage of newly available APIs and reduce the dependency of your code on third party dependencies where there is equivalent functionality in the Java standard library.,Basic building blocks for transforming Java code.,, diff --git a/src/test/java/org/openrewrite/java/migrate/ArrayStoreExceptionToTypeNotPresentExceptionTest.java b/src/test/java/org/openrewrite/java/migrate/ArrayStoreExceptionToTypeNotPresentExceptionTest.java index 59b8b9fc29..99f8fed2ed 100644 --- a/src/test/java/org/openrewrite/java/migrate/ArrayStoreExceptionToTypeNotPresentExceptionTest.java +++ b/src/test/java/org/openrewrite/java/migrate/ArrayStoreExceptionToTypeNotPresentExceptionTest.java @@ -17,10 +17,14 @@ import org.junit.jupiter.api.Test; import org.openrewrite.DocumentExample; +import org.openrewrite.java.marker.JavaProject; import org.openrewrite.test.RecipeSpec; import org.openrewrite.test.RewriteTest; +import org.openrewrite.test.TypeValidation; +import static org.openrewrite.Tree.randomId; import static org.openrewrite.java.Assertions.java; +import static org.openrewrite.kotlin.Assertions.kotlin; class ArrayStoreExceptionToTypeNotPresentExceptionTest implements RewriteTest { @@ -31,7 +35,7 @@ public void defaults(RecipeSpec spec) { @DocumentExample @Test - void replaceCaughtException() { + void alsoCatchTypeNotPresentException() { rewriteRun( //language=java java( @@ -64,12 +68,12 @@ public void testMethod() { try { Object o = "test"; o.getClass().getAnnotation(Override.class); - } catch (TypeNotPresentException e) { + } catch (ArrayStoreException | TypeNotPresentException e) { System.out.println("Caught Exception"); } try { Object.class.getAnnotation(Override.class); - } catch (TypeNotPresentException e) { + } catch (ArrayStoreException | TypeNotPresentException e) { System.out.println("Caught ArrayStoreException"); } } @@ -119,4 +123,995 @@ public void testMethod() { ) ); } + + @Test + void retainArrayStoreExceptionWhenBodyCanStillThrowIt() { + rewriteRun( + //language=java + java( + """ + import java.lang.annotation.Annotation; + + class Example { + void inspect(Class type, Class annotation, Object value) { + try { + type.getAnnotation(annotation); + Object[] values = new String[1]; + values[0] = value; + } catch (ArrayStoreException e) { + recover(e); + } + } + + void recover(RuntimeException e) { + } + } + """, + """ + import java.lang.annotation.Annotation; + + class Example { + void inspect(Class type, Class annotation, Object value) { + try { + type.getAnnotation(annotation); + Object[] values = new String[1]; + values[0] = value; + } catch (ArrayStoreException | TypeNotPresentException e) { + recover(e); + } + } + + void recover(RuntimeException e) { + } + } + """ + ) + ); + } + + @Test + void lookupInTryWithResourcesInitializer() { + rewriteRun( + //language=java + java( + """ + class Example { + void inspect(Class type) { + try (Resource resource = new Resource(type.getAnnotation(Override.class))) { + resource.use(); + } catch (ArrayStoreException e) { + recover(e); + } + } + + void recover(RuntimeException e) { + } + + static class Resource implements AutoCloseable { + Resource(Object annotation) { + } + + void use() { + } + + @Override + public void close() { + } + } + } + """, + """ + class Example { + void inspect(Class type) { + try (Resource resource = new Resource(type.getAnnotation(Override.class))) { + resource.use(); + } catch (ArrayStoreException | TypeNotPresentException e) { + recover(e); + } + } + + void recover(RuntimeException e) { + } + + static class Resource implements AutoCloseable { + Resource(Object annotation) { + } + + void use() { + } + + @Override + public void close() { + } + } + } + """ + ) + ); + } + + /** + * Only the try's resources and body are protected by its catches: a call in a finally block, a sibling + * catch, or a body that runs after the try never throws into these handlers. The immediately invoked + * lambda and the instance initializer do run inside the protected region, but the bodies of lambdas and + * classes created in the try are conservatively skipped as a whole, costing only a missed migration. + */ + @Test + void retainWhenGetAnnotationIsOutsideTheProtectedRegion() { + rewriteRun( + //language=java + java( + """ + import java.lang.annotation.Annotation; + import java.util.List; + import java.util.function.Function; + + class Example { + void inFinally(Class type, Object value) { + try { + Object[] values = new String[1]; + values[0] = value; + } catch (ArrayStoreException e) { + recover(e); + } finally { + type.getAnnotation(Override.class); + } + } + + void inSiblingCatch(Class type, Object value) { + try { + Object[] values = new String[1]; + values[0] = value; + } catch (ArrayStoreException e) { + recover(e); + } catch (IllegalArgumentException e) { + type.getAnnotation(Override.class); + } + } + + Runnable inDeferredLambda(Class type, Object value) { + try { + Object[] values = new String[1]; + values[0] = value; + return () -> type.getAnnotation(Override.class); + } catch (ArrayStoreException e) { + recover(e); + return () -> { + }; + } + } + + Function, Override> inMethodReference(Class type, Object value) { + try { + Object[] values = new String[1]; + values[0] = value; + return type::getAnnotation; + } catch (ArrayStoreException e) { + recover(e); + return null; + } + } + + void inImmediatelyInvokedLambda(List> types, Object value) { + try { + Object[] values = new String[1]; + values[0] = value; + types.forEach(type -> type.getAnnotation(Override.class)); + } catch (ArrayStoreException e) { + recover(e); + } + } + + Runnable inAnonymousClass(Class type, Object value) { + try { + Object[] values = new String[1]; + values[0] = value; + return new Runnable() { + @Override + public void run() { + type.getAnnotation(Override.class); + } + }; + } catch (ArrayStoreException e) { + recover(e); + return null; + } + } + + Runnable inLocalClass(Class type, Object value) { + try { + Object[] values = new String[1]; + values[0] = value; + class Inspector implements Runnable { + @Override + public void run() { + type.getAnnotation(Override.class); + } + } + return new Inspector(); + } catch (ArrayStoreException e) { + recover(e); + return null; + } + } + + Runnable inAnonymousClassInstanceInitializer(Class type, Object value) { + try { + Object[] values = new String[1]; + values[0] = value; + return new Runnable() { + final Annotation a = type.getAnnotation(Override.class); + + @Override + public void run() { + } + }; + } catch (ArrayStoreException e) { + recover(e); + return null; + } + } + + void recover(RuntimeException e) { + } + } + """ + ) + ); + } + + /** + * A sibling catch of {@code TypeNotPresentException} or a supertype already handles it, a catch of a + * subclass would become unreachable, and a multi-catch parameter is typed as the least upper bound + * rather than {@code ArrayStoreException}, so none of these tries change. + */ + @Test + void retainWhenTypeNotPresentExceptionIsAlreadyHandled() { + rewriteRun( + //language=java + java( + """ + class Example { + void existingCatch(Class type) { + try { + type.getAnnotation(Override.class); + } catch (ArrayStoreException e) { + recover(e); + } catch (TypeNotPresentException e) { + recover(e); + } + } + + void existingMultiCatch(Class type) { + try { + type.getAnnotation(Override.class); + } catch (ArrayStoreException | IllegalStateException e) { + recover(e); + } + } + + void existingBroaderCatch(Class type) { + try { + type.getAnnotation(Override.class); + } catch (ArrayStoreException e) { + recover(e); + } catch (RuntimeException e) { + recover(e); + } + } + + void existingSubclassCatch(Class type) { + try { + type.getAnnotation(Override.class); + } catch (ArrayStoreException e) { + recover(e); + } catch (MissingType e) { + recover(e); + } + } + + void recover(RuntimeException e) { + } + + static class MissingType extends TypeNotPresentException { + MissingType() { + super("Missing", null); + } + } + } + """ + ) + ); + } + + /** + * The last two handlers mirror real consumers: WildFly's {@code BusinessViewAnnotationProcessor} throws + * its own deployment error without referencing the parameter, and Grails' {@code DomainClassArtefactHandler} + * swallows the failure with an empty catch. + */ + @Test + void widenCatchWhenHandlerAcceptsRuntimeException() { + // %1$s fills every catch site with the single formatted argument + //language=java + var source = """ + class Example { + void logAndRethrow(Class type) { + try { + type.getAnnotation(Override.class); + } catch (%1$s e) { + System.out.println("failed: " + e.getMessage()); + e.printStackTrace(); + throw e; + } + } + + void wrap(Class type) { + try { + type.getAnnotation(Override.class); + } catch (%1$s e) { + throw new IllegalStateException("wrap", e); + } + } + + void assign(Class type) { + try { + type.getAnnotation(Override.class); + } catch (%1$s e) { + RuntimeException cause = e; + cause = e; + recover(cause); + recover(e); + } + } + + void requireAnnotation(Class type) { + try { + type.getAnnotation(Override.class); + } catch (%1$s e) { + throw new IllegalStateException("missing class in annotation on " + type.getName()); + } + } + + boolean isDomainClass(Class type) { + Deprecated artefact = null; + try { + artefact = type.getAnnotation(Deprecated.class); + } catch (%1$s e) { + // a reference to a class that no longer exists + } + return artefact != null; + } + + void recover(RuntimeException e) { + } + } + """; + rewriteRun( + java( + source.formatted("ArrayStoreException"), + source.formatted("ArrayStoreException | TypeNotPresentException") + ) + ); + } + + /** + * The allow list deliberately stops at the common handler shapes; any use of the parameter it does not + * recognize retains the catch, which only costs a migration that is not applied. + */ + @Test + void retainCatchWhoseHandlerUsesTheExceptionBeyondTheAllowList() { + rewriteRun( + //language=java + java( + """ + import java.util.Objects; + import java.util.function.Supplier; + + class Example { + RuntimeException returnBroaderType(Class type) { + try { + type.getAnnotation(Override.class); + return null; + } catch (ArrayStoreException e) { + return e; + } + } + + Runnable methodReference(Class type) { + try { + type.getAnnotation(Override.class); + return null; + } catch (ArrayStoreException e) { + return e::printStackTrace; + } + } + + Supplier expressionLambda(Class type) { + try { + type.getAnnotation(Override.class); + return null; + } catch (ArrayStoreException e) { + return () -> e; + } + } + + void genericInference(Class type) { + try { + type.getAnnotation(Override.class); + } catch (ArrayStoreException e) { + Objects.requireNonNull(e); + } + } + + void ternary(Class type, boolean flag) { + try { + type.getAnnotation(Override.class); + } catch (ArrayStoreException e) { + RuntimeException chosen = flag ? e : null; + recover(chosen); + } + } + + void appendToMessage(Class type) { + try { + type.getAnnotation(Override.class); + } catch (ArrayStoreException e) { + String message = "failed: "; + message += e; + System.out.println(message); + } + } + + void synchronize(Class type) { + try { + type.getAnnotation(Override.class); + } catch (ArrayStoreException e) { + synchronized (e) { + recover(e); + } + } + } + + void passToVarargs(Class type) { + try { + type.getAnnotation(Override.class); + } catch (ArrayStoreException e) { + log("failed", e); + } + } + + void passToNarrowerParameter(Class type) { + try { + type.getAnnotation(Override.class); + } catch (ArrayStoreException e) { + recoverNarrow(e); + } + } + + void assignToNarrowerVariable(Class type) { + try { + type.getAnnotation(Override.class); + } catch (ArrayStoreException e) { + ArrayStoreException copy = e; + recover(copy); + } + } + + void storeInArrayInitializer(Class type) { + try { + type.getAnnotation(Override.class); + } catch (ArrayStoreException e) { + ArrayStoreException[] all = {e}; + recover(all[0]); + } + } + + void castToNarrowerType(Class type) { + try { + type.getAnnotation(Override.class); + } catch (ArrayStoreException e) { + // The cast still compiles, but would throw for the TypeNotPresentException values a widened handler receives + Object narrowed = (ArrayStoreException) e; + recover(e); + } + } + + void reassignParameter(Class type) { + try { + type.getAnnotation(Override.class); + } catch (ArrayStoreException e) { + // A multi-catch parameter is implicitly final, so this would not compile widened + e = new ArrayStoreException("wrapped"); + recover(e); + } + } + + void readTheExceptionClass(Class type) { + try { + type.getAnnotation(Override.class); + } catch (ArrayStoreException e) { + // Per JLS 4.3.2 `e.getClass()` is typed over the receiver's static type, so it widens with it + Class narrow = e.getClass(); + Class wide = e.getClass(); + recover(e); + } + } + + void log(String message, ArrayStoreException... exceptions) { + } + + void recover(RuntimeException e) { + } + + void recoverNarrow(ArrayStoreException e) { + } + } + """ + ) + ); + } + + /** + * An unresolvable receiving method has unknowable requirements, so the catch is left alone. + */ + @Test + void retainCatchThatPassesTheExceptionToAnUnresolvableMethod() { + rewriteRun( + spec -> spec.typeValidationOptions(TypeValidation.none()), + //language=java + java( + """ + class Example { + void inspect(Class type) { + try { + type.getAnnotation(Override.class); + } catch (ArrayStoreException e) { + Unknown.log(e); + } + } + } + """ + ) + ); + } + + /** + * Only the caught exception is implicitly final; an unrelated variable that happens to share its name is not. + */ + @Test + void alsoCatchWhenAnUnrelatedSameNamedVariableIsAssigned() { + rewriteRun( + //language=java + java( + """ + class Example { + void inspect(Class type) { + try { + type.getAnnotation(Override.class); + } catch (ArrayStoreException e) { + recover(e); + run(new Runnable() { + String e; + + @Override + public void run() { + e = "unrelated"; + } + }); + } + } + + void recover(RuntimeException e) { + } + + void run(Runnable runnable) { + } + } + """, + """ + class Example { + void inspect(Class type) { + try { + type.getAnnotation(Override.class); + } catch (ArrayStoreException | TypeNotPresentException e) { + recover(e); + run(new Runnable() { + String e; + + @Override + public void run() { + e = "unrelated"; + } + }); + } + } + + void recover(RuntimeException e) { + } + + void run(Runnable runnable) { + } + } + """ + ) + ); + } + + /** + * Kotlin has no multi-catch, so Kotlin sources are left alone. + */ + @Test + void kotlinFileNotChanged() { + rewriteRun( + //language=kotlin + kotlin( + """ + class Example { + fun inspect(type: Class<*>) { + try { + type.getAnnotation(Override::class.java) + } catch (e: ArrayStoreException) { + recover(e) + } + } + + fun recover(e: RuntimeException) { + } + } + """ + ) + ); + } + + @Test + void retainArrayStoreExceptionWhenLookupIsNotTypeAttributed() { + rewriteRun( + spec -> spec.typeValidationOptions(TypeValidation.none()), + //language=java + java( + """ + class Example { + void inspect(Unknown type) { + try { + type.getAnnotation(Override.class); + } catch (ArrayStoreException e) { + recover(e); + } + } + + void recover(RuntimeException e) { + } + } + """ + ) + ); + } + + /** + * Every {@code TypeNotPresentException} the inner try does not catch reaches the enclosing handler, so + * widening the inner catch would steal the exception from it. + */ + @Test + void retainWhenEnclosingTryHandlesTypeNotPresentException() { + rewriteRun( + //language=java + java( + """ + class Example { + void inspect(Class type) { + try { + try { + type.getAnnotation(Override.class); + } catch (ArrayStoreException e) { + recover(e); + } + } catch (TypeNotPresentException e) { + handleMissingType(e); + } + } + + void recover(RuntimeException e) { + } + + void handleMissingType(TypeNotPresentException e) { + } + } + """ + ) + ); + } + + /** + * From inside a catch block the enclosing try's catches are no longer reachable, so an inner try there is + * not stealing from them and still migrates. + */ + @Test + void alsoCatchInsideHandlerOfEnclosingTry() { + rewriteRun( + //language=java + java( + """ + class Example { + void inspect(Class type) { + try { + type.getTypeParameters(); + } catch (TypeNotPresentException missing) { + try { + type.getAnnotation(Override.class); + } catch (ArrayStoreException e) { + recover(e); + } + } + } + + void recover(RuntimeException e) { + } + } + """, + """ + class Example { + void inspect(Class type) { + try { + type.getTypeParameters(); + } catch (TypeNotPresentException missing) { + try { + type.getAnnotation(Override.class); + } catch (ArrayStoreException | TypeNotPresentException e) { + recover(e); + } + } + } + + void recover(RuntimeException e) { + } + } + """ + ) + ); + } + + /** + * The nested class shadows the simple name, and because it extends {@code RuntimeException} the simple + * name would even compile while binding the catch to the wrong type; the file is left unchanged. + */ + @Test + void retainWhenNestedClassShadowsSimpleName() { + rewriteRun( + //language=java + java( + """ + class Example { + void inspect(Class type) { + try { + type.getAnnotation(Override.class); + } catch (ArrayStoreException e) { + recover(e); + } + } + + void recover(RuntimeException e) { + } + + static class TypeNotPresentException extends RuntimeException { + } + } + """ + ) + ); + } + + @Test + void retainWhenImportShadowsSimpleName() { + rewriteRun( + //language=java + java( + """ + package shadow; + + public class TypeNotPresentException { + } + """ + ), + //language=java + java( + """ + package example; + + import shadow.TypeNotPresentException; + + class Example { + void inspect(Class type) { + try { + type.getAnnotation(Override.class); + } catch (ArrayStoreException e) { + recover(e); + } + } + + void recover(RuntimeException e) { + } + } + """ + ) + ); + } + + /** + * A same-package class shadows the simple name even when this file never references it, which is why the + * scanner records every source-declared class of this name. + */ + @Test + void retainWhenSamePackageClassShadowsSimpleName() { + rewriteRun( + //language=java + java( + """ + package example; + + class TypeNotPresentException { + } + """ + ), + //language=java + java( + """ + package example; + + class Example { + void inspect(Class type) { + try { + type.getAnnotation(Override.class); + } catch (ArrayStoreException e) { + recover(e); + } + } + + void recover(RuntimeException e) { + } + } + """ + ) + ); + } + + /** + * The scanner's record of declared {@code TypeNotPresentException} classes is scoped per + * {@code JavaProject} marker: a class declared in one module of a multi-module repository is not on + * another module's horizon, so there the simple name is emitted. + */ + @Test + void alsoCatchSimpleNameWhenShadowingClassIsDeclaredInAnotherJavaProject() { + var moduleA = new JavaProject(randomId(), "module-a", null); + var moduleB = new JavaProject(randomId(), "module-b", null); + rewriteRun( + //language=java + java( + """ + package example; + + class TypeNotPresentException { + } + """, + spec -> spec.markers(moduleA) + ), + //language=java + java( + """ + package example; + + class Example { + void inspect(Class type) { + try { + type.getAnnotation(Override.class); + } catch (ArrayStoreException e) { + recover(e); + } + } + + void recover(RuntimeException e) { + } + } + """, + """ + package example; + + class Example { + void inspect(Class type) { + try { + type.getAnnotation(Override.class); + } catch (ArrayStoreException | TypeNotPresentException e) { + recover(e); + } + } + + void recover(RuntimeException e) { + } + } + """, + spec -> spec.markers(moduleB) + ) + ); + } + + /** + * Within one {@code JavaProject} the declaration still shadows: the marked sibling source is left + * unchanged exactly as an unmarked one is. + */ + @Test + void retainWhenShadowingClassIsDeclaredInTheSameJavaProject() { + var module = new JavaProject(randomId(), "module-a", null); + rewriteRun( + //language=java + java( + """ + package example; + + class TypeNotPresentException { + } + """, + spec -> spec.markers(module) + ), + //language=java + java( + """ + package example; + + class Example { + void inspect(Class type) { + try { + type.getAnnotation(Override.class); + } catch (ArrayStoreException e) { + recover(e); + } + } + + void recover(RuntimeException e) { + } + } + """, + spec -> spec.markers(module) + ) + ); + } + + /** + * A nested class inherited from a supertype shadows the simple name inside the subclass; it would compile + * while binding the catch to the inherited type, so the file is left unchanged. + */ + @Test + void retainWhenInheritedNestedClassShadowsSimpleName() { + rewriteRun( + //language=java + java( + """ + package example; + + public class Base { + public static class TypeNotPresentException extends RuntimeException { + } + } + """ + ), + //language=java + java( + """ + package example; + + class Example extends Base { + void inspect(Class type) { + try { + type.getAnnotation(Override.class); + } catch (ArrayStoreException e) { + recover(e); + } + } + + void recover(RuntimeException e) { + } + } + """ + ) + ); + } + }