Java: support parameterized types in ChangeMethodInvocationReturnType#8043
Java: support parameterized types in ChangeMethodInvocationReturnType#8043MBoegers wants to merge 8 commits into
ChangeMethodInvocationReturnType#8043Conversation
`JavaType.buildType("java.util.Set<java.lang.String>")` does not understand
generics, so the recipe folded the `<...>` suffix into a malformed `ShallowClass`
and emitted a bare `J.Identifier` whose name still contained `<...>`, with no
imports for the type parameters - producing non-compilable output.
Add `TypeUtils.buildTypeTree(String)`, a single string -> TypeTree builder that
handles parameterized types (nested type arguments), arrays, wildcards,
primitives and `java.lang` shortening, and consolidate the near-duplicate
`AddMethodParameter.createTypeTree` into it. The recipe now builds a proper
parameterized type expression, derives its JavaType via `getType()`, shortens
the fully-qualified tree (adding imports for every nested type), and recursively
removes imports for the replaced type.
Closes #7502
| public static TypeTree buildTypeTree(String typeName) { | ||
| if (typeName.endsWith("]")) { | ||
| TypeTree elementType = buildTypeTree(typeName.substring(0, typeName.lastIndexOf('['))); | ||
| return new J.ArrayType( | ||
| randomId(), | ||
| Space.EMPTY, | ||
| Markers.EMPTY, | ||
| elementType, | ||
| null, | ||
| JLeftPadded.build(Space.EMPTY), | ||
| buildType(typeName) | ||
| ); | ||
| } |
There was a problem hiding this comment.
The challenge I have with this mechanism is that it is not classpath-aware. If you use this to build a type tree for anything not in the standard library you get a stubbed / shallow type.
Is there a reason a type tree produced by a context-insensitive JavaTemplate will not serve?
There was a problem hiding this comment.
You were right, and I had another deep thinking here. It should now do the trick.
…th-aware Addresses review feedback on #8043: the previous `TypeUtils.buildTypeTree` helper synthesized shallow `ShallowClass` stubs for any type outside the standard library. Instead, parse the new return type in type position via `JavaTemplate` so it is resolved against the classpath (fully attributed), splice the resolved type expression into the existing declaration (preserving modifiers, names and initializer), then shorten + add imports. When the type cannot be resolved against the classpath the declaration is left unchanged. This reverts the `TypeUtils` and `AddMethodParameter` changes from the previous commit, since the shared `buildTypeTree` builder is no longer used.
- Update the matched invocation's return type in `visitMethodInvocation` (where every invocation is already visited) instead of a separate nested visitor. - Use early returns for the guard conditions in `visitVariableDeclarations`. - Assert the resolved type is not a `ShallowClass` in the classpath test.
…ype` Use the built-in `maybeRemoveImport(getTypeAsFullyQualified())` for the old raw type instead of a hand-rolled recursive walk over type parameters, and document why `java.lang` is stripped from the template type up front.
…fore-javatypetree # Conflicts: # rewrite-java/src/main/java/org/openrewrite/java/ChangeMethodInvocationReturnType.java
| // `ShortenFullyQualifiedTypeReferences` deliberately leaves `java.lang.*` fully qualified, so strip it here. | ||
| String templateType = newReturnType.replaceAll("\\bjava\\.lang\\.([A-Z][A-Za-z0-9_]*)(?![.A-Za-z0-9_])", "$1"); |
There was a problem hiding this comment.
ShortenFullyQualifiedTypeReferences only deliberately leaves java.lang when there is a name conflict. For example it would leave java.lang.Deprecated if a user-defined type com.foo.Deprecated existed on the classpath.
So this comment is either indicative of a bug in ShortenFullyQualifiedTypeReferences not shortening something it should, or this recipe shortening something it shouldn't.
There was a problem hiding this comment.
There is even a test for this that requires this. it was added with 2ea58e616 as one precise commit, so this is on purpose. Let me change the test and not shorten java.lang.* as it is the expected OR behavior.
…rnType` The default `JavaTemplate` parser only resolves JDK types, so a non-JDK `newReturnType` (e.g. `jakarta.validation.ConstraintViolation<...>`) came back as `JavaType.Unknown` and the recipe silently no-op'd. Synthesize a minimal source stub (with the correct type-parameter arity) for every non-`java.` type referenced in `newReturnType` and feed it to the template parser via `dependsOn`, following the pattern in `ChangeStaticFieldToMethod`. The type then attributes to a real class without the declaring artifact on the classpath.
…thodInvocationReturnType`
…turnType` Leave `java.lang.*` fully qualified in the output on purpose; the proper fix belongs in `ShortenFullyQualifiedTypeReferences` (separate PR). Update the generic tests accordingly, and note in the option description that `java.lang` type arguments may be given by simple name (e.g. `java.util.List<String>`).
ChangeMethodInvocationReturnType#7509 — thanks @timtebeek for the original approach and reproducers.Problem
ChangeMethodInvocationReturnTypebreaks on generic return types.JavaType.buildType("java.util.Set<java.lang.String>")doesn't understand generics, so it folds the<...>suffix into a malformedShallowClassand the recipe emits a bareJ.Identifierwhose simple name still contains<...>— non-compilable output with missing imports for the type parameters.Change
TypeUtils.buildTypeTree(String)— a singleString→TypeTreebuilder supporting parameterized types with nested type arguments (Map<String, List<Integer>>), arrays, wildcards (?,? extends X,? super X), primitives andjava.langshortening. ItsJavaTypeis resolved with the same parser, sobuildTypeTree(name).getType()yields the matching type.AddMethodParameter.createTypeTreeintobuildTypeTree, fixing latent gaps: depth-aware splitting of multi-argument nested generics, a space after each comma, and? superbounds.ChangeMethodInvocationReturnTypenow builds a proper parameterized type expression, derives the newJavaTypeviagetType(), shortens the fully-qualified tree viaImportService(importing the raw type and every type parameter), and recursively removes imports for the replaced type. The existinginitializedByMatchguard is preserved.The core
JavaType/TypeTreeinterfaces are intentionally left untouched; the logic lives inTypeUtils. Promoting it intoJavaType.buildTypelater would be a clean follow-up.Test plan
List<String>→Set<String>), nested generics (Map<String, List<Integer>>), a depth-stressing case (Map<String, Map<Integer, Long>>) that the old naive split breaks, and parameterized → raw (List<String>→Object)../gradlew :rewrite-java:testpasses;AddMethodParameterTest, TCKTypeTreeTest,ChangeTypeTestgreen;licenseFormatclean.closing https://github.com/moderneinc/customer-requests/issues/2434