From af3e29e21abf9513a051a4940411c69e38ad6d78 Mon Sep 17 00:00:00 2001 From: Jente Sondervorst Date: Fri, 29 May 2026 13:51:59 +0200 Subject: [PATCH] Add optional `ecosystem` parameter to `ChangeType` `ChangeType` applies to every `JavaSourceFile`. Because Python and JavaScript reuse the Java LST model (their compilation units implement `JavaSourceFile`), a JVM-targeted type change still runs its full `UsesType` precondition scan over every Python/JS file, even though those languages can never reference a JVM type. Add an optional `ecosystem` option (`jvm`, `python`, `javascript`) that scopes the change to a single ecosystem. Default (null) is unchanged: the recipe runs on all source files. Reference-based files (XML, properties) are always processed since they may carry type references. --- .../org/openrewrite/java/ChangeTypeTest.java | 23 ++++++++ .../java/org/openrewrite/java/ChangeType.java | 56 +++++++++++++++++++ .../python/ChangeTypeIntegTest.java | 18 ++++++ 3 files changed, 97 insertions(+) diff --git a/rewrite-java-test/src/test/java/org/openrewrite/java/ChangeTypeTest.java b/rewrite-java-test/src/test/java/org/openrewrite/java/ChangeTypeTest.java index 4b20787d2d3..e3e2e351ac1 100644 --- a/rewrite-java-test/src/test/java/org/openrewrite/java/ChangeTypeTest.java +++ b/rewrite-java-test/src/test/java/org/openrewrite/java/ChangeTypeTest.java @@ -2647,6 +2647,29 @@ class Test { ); } + @Test + void appliesToJavaFilesWhenEcosystemIsJvm() { + // A Java source file is in the "jvm" ecosystem, so a jvm-scoped change still applies. + rewriteRun( + spec -> spec.recipe(new ChangeType("java.lang.Integer", "java.lang.Long", true, "jvm")), + java( + "public class ThinkPositive { private Integer fred = 1;}", + "public class ThinkPositive { private Long fred = 1;}" + ) + ); + } + + @Test + void skipsJavaFilesWhenEcosystemIsNonJvm() { + // Scoping to a non-JVM ecosystem must skip JVM (Java) source files entirely. + rewriteRun( + spec -> spec.recipe(new ChangeType("java.lang.Integer", "java.lang.Long", true, "python")), + java( + "public class ThinkPositive { private Integer fred = 1;}" + ) + ); + } + @Test void changeTypeAddsExplicitImportWhenStarImportsWouldBeAmbiguous() { rewriteRun( diff --git a/rewrite-java/src/main/java/org/openrewrite/java/ChangeType.java b/rewrite-java/src/main/java/org/openrewrite/java/ChangeType.java index 49ace100312..8759e7d3db3 100644 --- a/rewrite-java/src/main/java/org/openrewrite/java/ChangeType.java +++ b/rewrite-java/src/main/java/org/openrewrite/java/ChangeType.java @@ -15,6 +15,7 @@ */ package org.openrewrite.java; +import com.fasterxml.jackson.annotation.JsonCreator; import lombok.EqualsAndHashCode; import lombok.Value; import org.jspecify.annotations.Nullable; @@ -56,6 +57,18 @@ public class ChangeType extends Recipe { @Nullable Boolean ignoreDefinition; + @Option(displayName = "Ecosystem", + description = "Restrict the type change to source files of a given ecosystem: `jvm` (Java, Kotlin, " + + "Groovy), `python`, or `javascript`. Python and JavaScript reuse the Java LST model (their " + + "compilation units implement `JavaSourceFile`), so without this filter a change targeting one " + + "ecosystem still scans the others' source files unnecessarily. Reference-based files (such as " + + "XML and properties) are always processed because they may carry type references. When `null` " + + "(the default), the change is applied to all source files.", + valid = {"jvm", "python", "javascript"}, + required = false) + @Nullable + String ecosystem; + String displayName = "Change type"; @Override @@ -82,6 +95,46 @@ public String getInstanceNameSuffix() { String description = "Change a given type to another."; + public ChangeType(String oldFullyQualifiedTypeName, String newFullyQualifiedTypeName, @Nullable Boolean ignoreDefinition) { + this(oldFullyQualifiedTypeName, newFullyQualifiedTypeName, ignoreDefinition, null); + } + + @JsonCreator + public ChangeType(String oldFullyQualifiedTypeName, String newFullyQualifiedTypeName, @Nullable Boolean ignoreDefinition, @Nullable String ecosystem) { + this.oldFullyQualifiedTypeName = oldFullyQualifiedTypeName; + this.newFullyQualifiedTypeName = newFullyQualifiedTypeName; + this.ignoreDefinition = ignoreDefinition; + this.ecosystem = ecosystem; + } + + private boolean skipForEcosystem(JavaSourceFile cu) { + if (ecosystem == null || ecosystem.isEmpty()) { + return false; + } + String impl = cu.getClass().getName(); + String fileEcosystem; + if (impl.startsWith("org.openrewrite.python.")) { + fileEcosystem = "python"; + } else if (impl.startsWith("org.openrewrite.javascript.")) { + fileEcosystem = "javascript"; + } else { + // java, kotlin, groovy, scala, ... + fileEcosystem = "jvm"; + } + return !normalizeEcosystem(ecosystem).equals(fileEcosystem); + } + + private static String normalizeEcosystem(String ecosystem) { + switch (ecosystem.toLowerCase()) { + case "js": + return "javascript"; + case "py": + return "python"; + default: + return ecosystem.toLowerCase(); + } + } + @Override public TreeVisitor getVisitor() { TreeVisitor condition = new TreeVisitor() { @@ -90,6 +143,9 @@ public TreeVisitor getVisitor() { stopAfterPreVisit(); if (tree instanceof JavaSourceFile) { JavaSourceFile cu = (JavaSourceFile) tree; + if (skipForEcosystem(cu)) { + return tree; + } if (!Boolean.TRUE.equals(ignoreDefinition) && containsClassDefinition(cu, oldFullyQualifiedTypeName)) { return SearchResult.found(cu); } diff --git a/rewrite-python/src/integTest/java/org/openrewrite/python/ChangeTypeIntegTest.java b/rewrite-python/src/integTest/java/org/openrewrite/python/ChangeTypeIntegTest.java index 7bb5f9f727b..67f3e5e43c1 100644 --- a/rewrite-python/src/integTest/java/org/openrewrite/python/ChangeTypeIntegTest.java +++ b/rewrite-python/src/integTest/java/org/openrewrite/python/ChangeTypeIntegTest.java @@ -57,6 +57,24 @@ void changeBuiltinType() { ); } + @Test + void skippedWhenEcosystemIsJvm() { + // Python compilation units implement JavaSourceFile, so ChangeType would otherwise traverse them. + // Scoping the change to the "jvm" ecosystem must skip Python source files entirely (leaving them + // unchanged and avoiding the cost of scanning them). + rewriteRun( + spec -> spec + .typeValidationOptions(TypeValidation.none()) + .recipe(new ChangeType("str", "String", false, "jvm")), + python( + """ + result = "hello".upper() + """ + // unchanged: skipped because this is a Python (non-JVM) source file + ) + ); + } + // Note: ChangeType on module references (like os.path -> pathlib) requires // additional work to handle Python's module import structure properly. // Currently ChangeType works on type attribution but may not work on