Skip to content

Java: support parameterized types in ChangeMethodInvocationReturnType#8043

Open
MBoegers wants to merge 8 commits into
mainfrom
MBoegers/typeutils-before-javatypetree
Open

Java: support parameterized types in ChangeMethodInvocationReturnType#8043
MBoegers wants to merge 8 commits into
mainfrom
MBoegers/typeutils-before-javatypetree

Conversation

@MBoegers

@MBoegers MBoegers commented Jun 17, 2026

Copy link
Copy Markdown
Contributor

Problem

ChangeMethodInvocationReturnType breaks on generic return types. JavaType.buildType("java.util.Set<java.lang.String>") doesn't understand generics, so it folds the <...> suffix into a malformed ShallowClass and the recipe emits a bare J.Identifier whose simple name still contains <...> — non-compilable output with missing imports for the type parameters.

Change

  • Add TypeUtils.buildTypeTree(String) — a single StringTypeTree builder supporting parameterized types with nested type arguments (Map<String, List<Integer>>), arrays, wildcards (?, ? extends X, ? super X), primitives and java.lang shortening. Its JavaType is resolved with the same parser, so buildTypeTree(name).getType() yields the matching type.
  • Consolidate the near-duplicate hand-rolled AddMethodParameter.createTypeTree into buildTypeTree, fixing latent gaps: depth-aware splitting of multi-argument nested generics, a space after each comma, and ? super bounds.
  • ChangeMethodInvocationReturnType now builds a proper parameterized type expression, derives the new JavaType via getType(), shortens the fully-qualified tree via ImportService (importing the raw type and every type parameter), and recursively removes imports for the replaced type. The existing initializedByMatch guard is preserved.

The core JavaType/TypeTree interfaces are intentionally left untouched; the logic lives in TypeUtils. Promoting it into JavaType.buildType later would be a clean follow-up.

Test plan

  • Added the repro (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:test passes; AddMethodParameterTest, TCK TypeTreeTest, ChangeTypeTest green; licenseFormat clean.

closing https://github.com/moderneinc/customer-requests/issues/2434

`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
Comment on lines +113 to +125
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)
);
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You were right, and I had another deep thinking here. It should now do the trick.

MBoegers added 4 commits June 29, 2026 12:27
…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
@MBoegers MBoegers marked this pull request as ready for review June 30, 2026 10:33
@MBoegers MBoegers requested a review from sambsnyd June 30, 2026 10:33
@github-project-automation github-project-automation Bot moved this from In Progress to Ready to Review in OpenRewrite Jun 30, 2026
Comment on lines +89 to +90
// `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");

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

void dontModifyQualifiedJavaLangTypes() {
rewriteRun(
//language=java
java(
"""
class T {
java.lang.String s;
java.lang.Integer i;
java.lang.Object o;
}
"""
)
);
}

…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.
MBoegers added 2 commits July 1, 2026 13:28
…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>`).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Ready to Review

Development

Successfully merging this pull request may close these issues.

ChangeMethodInvocationReturnType is not handling parameterized types

3 participants