diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index ec2aee4ecf..22a15150fc 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,6 +1,7 @@ [versions] jline = '3.30.6' junit5 = '5.10.3' +openrewrite = '8.81.0' [libraries] archunit = { group = 'com.tngtech.archunit', name = 'archunit-junit5', version = '1.4.1' } @@ -12,6 +13,11 @@ jline-ffm = { group = 'org.jline', name = 'jline-terminal-ffm', version.ref = 'j jline-jni = { group = 'org.jline', name = 'jline-terminal-jni', version.ref = 'jline' } junit-platform = { group = 'org.junit', name = 'junit-bom', version.ref = 'junit5' } junit-jupiter-api = { group = 'org.junit.jupiter', name = 'junit-jupiter-api', version.ref = 'junit5'} +openrewrite-java = { group = 'org.openrewrite', name = 'rewrite-java', version.ref = 'openrewrite' } +openrewrite-java17 = { group = 'org.openrewrite', name = 'rewrite-java-17', version.ref = 'openrewrite' } +openrewrite-java21 = { group = 'org.openrewrite', name = 'rewrite-java-21', version.ref = 'openrewrite' } +openrewrite-test = { group = 'org.openrewrite', name = 'rewrite-test', version.ref = 'openrewrite' } + [bundles] jline-runtime = [ 'jline-ffm', 'jline-jni' ] \ No newline at end of file diff --git a/rhino-rewrite/build.gradle b/rhino-rewrite/build.gradle new file mode 100644 index 0000000000..8f63ca6f7f --- /dev/null +++ b/rhino-rewrite/build.gradle @@ -0,0 +1,30 @@ +plugins { + id 'rhino.java-conventions' +} + +dependencies { + implementation project(':rhino') + implementation libs.openrewrite.java + testImplementation project(':rhino') + testImplementation libs.openrewrite.test + testImplementation libs.openrewrite.java17 + testImplementation libs.openrewrite.java21 +} + +java { + toolchain { + languageVersion = JavaLanguageVersion.of(17) + } +} + +// Copy the rhino JAR into the expected location for OpenRewrite's classpathFromResources(). +// That method scans the classpath for JARs at "META-INF/rewrite/classpath/.jar". +def generatedTestResourcesDir = layout.buildDirectory.dir('generated-test-resources') +def copyRhinoJar = tasks.register('copyRhinoJarToRewriteClasspath', Copy) { + dependsOn ':rhino:jar' + from project(':rhino').tasks.named('jar', Jar).flatMap { it.archiveFile } + into generatedTestResourcesDir.map { it.dir('META-INF/rewrite/classpath') } + rename { 'rhino.jar' } +} +sourceSets.test.resources.srcDir(generatedTestResourcesDir) +processTestResources.dependsOn copyRhinoJar diff --git a/rhino-rewrite/src/main/java/org/mozilla/javascript/rewrite/MigrateCallableToVarScope.java b/rhino-rewrite/src/main/java/org/mozilla/javascript/rewrite/MigrateCallableToVarScope.java new file mode 100644 index 0000000000..83597768aa --- /dev/null +++ b/rhino-rewrite/src/main/java/org/mozilla/javascript/rewrite/MigrateCallableToVarScope.java @@ -0,0 +1,582 @@ +/* -*- Mode: java; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 4 -*- + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.javascript.rewrite; + +import org.openrewrite.*; +import org.openrewrite.internal.ListUtils; +import org.openrewrite.java.JavaIsoVisitor; +import org.openrewrite.java.MethodMatcher; +import org.openrewrite.java.tree.*; + +/** + * OpenRewrite recipe to migrate Rhino 1.x scope parameters to Rhino 2.0. + * + *

In Rhino 2.0, several key interfaces (Callable, Constructable, IdFunctionCall) have changed + * their 'scope' parameter from {@code Scriptable} to the new {@code VarScope} interface. + * Additionally, 'thisObj' may be migrated from {@code Scriptable} to {@code Object}. + */ +public class MigrateCallableToVarScope extends Recipe { + + @Option( + displayName = "Migrate scope to VarScope", + description = "Whether to migrate the 'scope' parameter from Scriptable to VarScope.", + required = false) + private final boolean migrateScope; + + @Option( + displayName = "Migrate thisObj to Object", + description = "Whether to migrate the 'thisObj' parameter from Scriptable to Object.", + required = false) + private final boolean migrateThisObj; + + @Option( + displayName = "Migrate Script.exec signatures", + description = "Whether to migrate Script.exec signatures.", + required = false) + private final boolean migrateScriptExec; + + public MigrateCallableToVarScope() { + this.migrateScope = true; + this.migrateThisObj = true; + this.migrateScriptExec = true; + } + + public MigrateCallableToVarScope( + boolean migrateScope, boolean migrateThisObj, boolean migrateScriptExec) { + this.migrateScope = migrateScope; + this.migrateThisObj = migrateThisObj; + this.migrateScriptExec = migrateScriptExec; + } + + public boolean isMigrateScope() { + return migrateScope; + } + + public boolean isMigrateThisObj() { + return migrateThisObj; + } + + @Override + public String getDisplayName() { + return "Migrate to Rhino 2.0"; + } + + @Override + public String getDescription() { + return "Migrates Rhino interfaces to 2.0 signatures."; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + MigrateCallableToVarScope that = (MigrateCallableToVarScope) o; + return migrateScope == that.migrateScope + && migrateThisObj == that.migrateThisObj + && migrateScriptExec == that.migrateScriptExec; + } + + @Override + public int hashCode() { + int result = (migrateScope ? 1 : 0); + result = 31 * result + (migrateThisObj ? 1 : 0); + result = 31 * result + (migrateScriptExec ? 1 : 0); + return result; + } + + private static final String SCRIPTABLE = "org.mozilla.javascript.Scriptable"; + private static final String VAR_SCOPE = "org.mozilla.javascript.VarScope"; + private static final String OBJECT = "java.lang.Object"; + + // Matches the signatures in Rhino interfaces. + // We match against the OLD signatures (Rhino 1.x) to find candidates, + // but we also have fallbacks for already partially migrated code. + private static final MethodMatcher CALL_MATCHER = + new MethodMatcher( + "org.mozilla.javascript.Callable" + + " call(org.mozilla.javascript.Context, *, *, java.lang.Object[])", + true); + + private static final MethodMatcher CONSTRUCT_MATCHER = + new MethodMatcher( + "org.mozilla.javascript.Constructable" + + " construct(org.mozilla.javascript.Context, *, java.lang.Object[])", + true); + + private static final MethodMatcher EXEC_ID_CALL_MATCHER = + new MethodMatcher( + "org.mozilla.javascript.IdFunctionCall" + + " execIdCall(org.mozilla.javascript.IdFunctionObject, org.mozilla.javascript.Context, *, *, java.lang.Object[])", + true); + + private static final MethodMatcher SCRIPT_EXEC_MATCHER = + new MethodMatcher( + "org.mozilla.javascript.Script exec(org.mozilla.javascript.Context, *, *)", + true); + + private static final MethodMatcher SCRIPT_EXEC_2_MATCHER = + new MethodMatcher( + "org.mozilla.javascript.Script exec(org.mozilla.javascript.Context, *)", true); + + private static final MethodMatcher INIT_STANDARD_OBJECTS_MATCHER = + new MethodMatcher("org.mozilla.javascript.Context initStandardObjects(..)", true); + + @Override + public TreeVisitor getVisitor() { + return new JavaIsoVisitor() { + + private J.VariableDeclarations migrateParameterType( + J.VariableDeclarations vd, String oldType, String newType) { + if (vd.getTypeExpression() == null) { + return null; + } + if (vd.getType() != null && !TypeUtils.isOfClassType(vd.getType(), oldType)) { + return null; + } + + JavaType.FullyQualified newFqType = JavaType.ShallowClass.build(newType); + J.VariableDeclarations updatedVd = vd.withType(newFqType); + + if (vd.getTypeExpression() != null) { + String simpleName = + newType.contains(".") + ? newType.substring(newType.lastIndexOf('.') + 1) + : newType; + if (vd.getTypeExpression() instanceof J.Identifier) { + J.Identifier identifier = (J.Identifier) vd.getTypeExpression(); + updatedVd = + updatedVd.withTypeExpression( + identifier.withSimpleName(simpleName).withType(newFqType)); + maybeAddImport(newType); + } else if (vd.getTypeExpression() instanceof J.FieldAccess) { + J.FieldAccess fieldAccess = (J.FieldAccess) vd.getTypeExpression(); + updatedVd = + updatedVd.withTypeExpression( + fieldAccess + .withName( + fieldAccess + .getName() + .withSimpleName(simpleName) + .withType(newFqType)) + .withType(newFqType)); + maybeAddImport(newType); + } + } + + updatedVd = + updatedVd.withVariables( + ListUtils.map( + updatedVd.getVariables(), + v -> + v.withName(v.getName().withType(newFqType)) + .withType(newFqType))); + return updatedVd; + } + + private java.util.Map getTargetParameterTypes( + JavaType type, int paramCount) { + java.util.Map targets = new java.util.HashMap<>(); + if (TypeUtils.isAssignableTo("org.mozilla.javascript.Callable", type)) { + if (migrateScope) targets.put(1, VAR_SCOPE); + if (migrateThisObj) targets.put(2, OBJECT); + } else if (TypeUtils.isAssignableTo("org.mozilla.javascript.Constructable", type)) { + if (migrateScope) targets.put(1, VAR_SCOPE); + } else if (TypeUtils.isAssignableTo( + "org.mozilla.javascript.IdFunctionCall", type)) { + if (migrateScope) targets.put(2, VAR_SCOPE); + if (migrateThisObj) targets.put(3, OBJECT); + } else if (TypeUtils.isAssignableTo("org.mozilla.javascript.Script", type)) { + if (paramCount == 3) { + if (migrateScriptExec) { + if (migrateScope) targets.put(1, VAR_SCOPE); + if (migrateThisObj) targets.put(2, OBJECT); + } + } else if (paramCount == 2) { + if (migrateScriptExec) { + if (migrateScope) targets.put(1, VAR_SCOPE); + } + } + } else if (type == null || type instanceof JavaType.Unknown) { + // Fallback heuristics based on parameter counts for common Rhino interfaces + if (paramCount == 4) { // Callable.call + if (migrateScope) targets.put(1, VAR_SCOPE); + if (migrateThisObj) targets.put(2, OBJECT); + } else if (paramCount == 3) { // Constructable.construct / Script.exec + if (migrateScope) targets.put(1, VAR_SCOPE); + } else if (paramCount == 2) { // Script.exec + if (migrateScope) targets.put(1, VAR_SCOPE); + } else if (paramCount == 5) { // IdFunctionCall.execIdCall + if (migrateScope) targets.put(2, VAR_SCOPE); + if (migrateThisObj) targets.put(3, OBJECT); + } + } + return targets; + } + + @Override + public J.Lambda visitLambda(J.Lambda lambda, ExecutionContext ctx) { + java.util.Map targets = + getTargetParameterTypes( + lambda.getType(), lambda.getParameters().getParameters().size()); + + if (targets.isEmpty()) { + return super.visitLambda(lambda, ctx); + } + + J.Lambda l = super.visitLambda(lambda, ctx); + boolean changed = false; + for (java.util.Map.Entry entry : targets.entrySet()) { + int idx = entry.getKey(); + String targetType = entry.getValue(); + + if (l.getParameters().getParameters().size() <= idx) { + continue; + } + + Object raw = l.getParameters().getParameters().get(idx); + if (!(raw instanceof J.VariableDeclarations)) { + continue; + } + + J.VariableDeclarations updatedVd = + migrateParameterType( + (J.VariableDeclarations) raw, SCRIPTABLE, targetType); + if (updatedVd != null) { + changed = true; + final int currentIdx = idx; + final J.VariableDeclarations replacement = updatedVd; + l = + l.withParameters( + l.getParameters() + .withParameters( + ListUtils.map( + l.getParameters().getParameters(), + (i, p) -> + i == currentIdx + ? replacement + : p))); + } + } + return changed ? l : super.visitLambda(lambda, ctx); + } + + @Override + public J.MemberReference visitMemberReference( + J.MemberReference memberReference, ExecutionContext ctx) { + J.MemberReference m = super.visitMemberReference(memberReference, ctx); + JavaType type = m.getType(); + if (type == null || type instanceof JavaType.Unknown) { + return m; + } + + java.util.Map targets = getTargetParameterTypes(type, -1); + if (targets.isEmpty()) { + return m; + } + + // Note: Method references don't have parameters in the reference itself. + // The target method's declaration must be migrated. + // visitMethodDeclaration handles this if the target method is an override + // of a Rhino interface method. If it's a non-override matching signature, + // we'd need to match its name/signature elsewhere. + + return m; + } + + @Override + public J.MethodDeclaration visitMethodDeclaration( + J.MethodDeclaration method, ExecutionContext ctx) { + + J.MethodDeclaration m = super.visitMethodDeclaration(method, ctx); + J.ClassDeclaration enclosingClass = + getCursor().firstEnclosingOrThrow(J.ClassDeclaration.class); + + java.util.Map targets = new java.util.HashMap<>(); + + if (CALL_MATCHER.matches(m, enclosingClass)) { + if (migrateScope) targets.put(1, VAR_SCOPE); + if (migrateThisObj) targets.put(2, OBJECT); + } else if (CONSTRUCT_MATCHER.matches(m, enclosingClass)) { + if (migrateScope) targets.put(1, VAR_SCOPE); + } else if (EXEC_ID_CALL_MATCHER.matches(m, enclosingClass)) { + if (migrateScope) targets.put(2, VAR_SCOPE); + if (migrateThisObj) targets.put(3, OBJECT); + } else if (migrateScriptExec && SCRIPT_EXEC_MATCHER.matches(m, enclosingClass)) { + if (migrateScope) targets.put(1, VAR_SCOPE); + if (migrateThisObj) targets.put(2, OBJECT); + } else if (migrateScriptExec && SCRIPT_EXEC_2_MATCHER.matches(m, enclosingClass)) { + if (migrateScope) targets.put(1, VAR_SCOPE); + } else { + // Fallback heuristics + String name = m.getSimpleName(); + int paramCount = m.getParameters().size(); + if ("call".equals(name) && paramCount == 4) { + if (migrateScope) targets.put(1, VAR_SCOPE); + if (migrateThisObj) targets.put(2, OBJECT); + } else if ("construct".equals(name) && paramCount == 3) { + if (migrateScope) targets.put(1, VAR_SCOPE); + } else if ("execIdCall".equals(name) && paramCount == 5) { + if (migrateScope) targets.put(2, VAR_SCOPE); + if (migrateThisObj) targets.put(3, OBJECT); + } else if ("exec".equals(name)) { + if (paramCount == 3) { + if (migrateScriptExec) { + if (migrateScope) targets.put(1, VAR_SCOPE); + if (migrateThisObj) targets.put(2, OBJECT); + } + } else if (paramCount == 2) { + if (migrateScriptExec) { + if (migrateScope) targets.put(1, VAR_SCOPE); + } + } + } + } + + if (targets.isEmpty()) { + return m; + } + + for (java.util.Map.Entry entry : targets.entrySet()) { + int idx = entry.getKey(); + String targetType = entry.getValue(); + + if (m.getParameters().size() <= idx) { + continue; + } + + Object raw = m.getParameters().get(idx); + if (!(raw instanceof J.VariableDeclarations)) { + continue; + } + + J.VariableDeclarations updatedVd = + migrateParameterType( + (J.VariableDeclarations) raw, SCRIPTABLE, targetType); + if (updatedVd != null) { + m = + m.withParameters( + ListUtils.map( + m.getParameters(), + (i, p) -> i == idx ? updatedVd : p)); + + // Documentation polish: Update Javadoc @param tags + final String paramName = updatedVd.getVariables().get(0).getSimpleName(); + final String newTypeSimple = + targetType.substring(targetType.lastIndexOf('.') + 1); + + final J.MethodDeclaration methodForLambda = m; + m = + m.withComments( + ListUtils.map( + m.getComments(), + c -> { + String rawText = + c.printComment(new Cursor(null, c)); + boolean isJavadoc = rawText.startsWith("/**"); + String text = rawText; + if (isJavadoc) { + text = + rawText.substring( + 3, rawText.length() - 2); + } else if (rawText.startsWith("/*")) { + text = + rawText.substring( + 2, rawText.length() - 2); + } else if (rawText.startsWith("//")) { + text = rawText.substring(2); + } + + String[] lines = text.split("\n"); + boolean modified = false; + for (int i = 0; i < lines.length; i++) { + if (lines[i].contains( + "@param " + paramName)) { + String newLine = + lines[i].replace( + "Scriptable", + newTypeSimple); + if (!newLine.equals(lines[i])) { + lines[i] = newLine; + modified = true; + } + } + } + + if (!modified) { + return c; + } + + String updatedText = String.join("\n", lines); + if (isJavadoc) { + updatedText = "*" + updatedText; + } + return new TextComment( + c.isMultiline(), + updatedText, + c.getSuffix(), + c.getMarkers()); + })); + } + } + + maybeRemoveImport(SCRIPTABLE); + return m; + } + + @Override + public J.MethodInvocation visitMethodInvocation( + J.MethodInvocation invocation, ExecutionContext ctx) { + + J.MethodInvocation inv = super.visitMethodInvocation(invocation, ctx); + java.util.Map targets = getTargetArgumentIndices(inv); + + if (targets.isEmpty()) { + return inv; + } + + for (java.util.Map.Entry entry : targets.entrySet()) { + int idx = entry.getKey(); + String targetType = entry.getValue(); + + if (inv.getArguments().size() <= idx) { + continue; + } + + Object rawArg = inv.getArguments().get(idx); + if (!(rawArg instanceof J.TypeCast)) { + continue; + } + + J.TypeCast cast = (J.TypeCast) rawArg; + TypeTree castType = cast.getClazz().getTree(); + if (!TypeUtils.isOfClassType(castType.getType(), SCRIPTABLE)) { + continue; + } + + JavaType.FullyQualified newFqType = JavaType.ShallowClass.build(targetType); + TypeTree newTypeTree; + + String simpleName = + targetType.contains(".") + ? targetType.substring(targetType.lastIndexOf('.') + 1) + : targetType; + + if (castType instanceof J.Identifier) { + J.Identifier id = (J.Identifier) castType; + newTypeTree = id.withSimpleName(simpleName).withType(newFqType); + } else if (castType instanceof J.FieldAccess) { + J.FieldAccess fa = (J.FieldAccess) castType; + newTypeTree = + fa.withName( + fa.getName() + .withSimpleName(simpleName) + .withType(newFqType)) + .withType(newFqType); + } else { + continue; + } + + J.TypeCast newCast = + cast.withClazz(cast.getClazz().withTree(newTypeTree)) + .withType(newFqType); + + final int currentIdx = idx; + final J.TypeCast replacement = newCast; + inv = + inv.withArguments( + ListUtils.map( + inv.getArguments(), + (i, arg) -> i == currentIdx ? replacement : arg)); + + maybeAddImport(targetType); + } + + return inv; + } + + private java.util.Map getTargetArgumentIndices( + J.MethodInvocation inv) { + java.util.Map targets = new java.util.HashMap<>(); + if (CALL_MATCHER.matches(inv)) { + if (migrateScope) targets.put(1, VAR_SCOPE); + if (migrateThisObj) targets.put(2, OBJECT); + } else if (CONSTRUCT_MATCHER.matches(inv)) { + if (migrateScope) targets.put(1, VAR_SCOPE); + } else if (EXEC_ID_CALL_MATCHER.matches(inv)) { + if (migrateScope) targets.put(2, VAR_SCOPE); + if (migrateThisObj) targets.put(3, OBJECT); + } else if (migrateScriptExec && SCRIPT_EXEC_MATCHER.matches(inv)) { + if (migrateScope) targets.put(1, VAR_SCOPE); + if (migrateThisObj) targets.put(2, OBJECT); + } else if (migrateScriptExec && SCRIPT_EXEC_2_MATCHER.matches(inv)) { + if (migrateScope) targets.put(1, VAR_SCOPE); + } else { + // Fallback heuristics + String name = inv.getSimpleName(); + int argCount = inv.getArguments().size(); + if ("call".equals(name) && argCount == 4) { + if (migrateScope) targets.put(1, VAR_SCOPE); + if (migrateThisObj) targets.put(2, OBJECT); + } else if ("construct".equals(name) && argCount == 3) { + if (migrateScope) targets.put(1, VAR_SCOPE); + } else if ("execIdCall".equals(name) && argCount == 5) { + if (migrateScope) targets.put(2, VAR_SCOPE); + if (migrateThisObj) targets.put(3, OBJECT); + } else if ("exec".equals(name)) { + if (argCount == 3) { + if (migrateScriptExec) { + if (migrateScope) targets.put(1, VAR_SCOPE); + if (migrateThisObj) targets.put(2, OBJECT); + } + } else if (argCount == 2) { + if (migrateScriptExec) { + if (migrateScope) targets.put(1, VAR_SCOPE); + } + } + } + } + return targets; + } + + @Override + public J.VariableDeclarations visitVariableDeclarations( + J.VariableDeclarations multiVariable, ExecutionContext ctx) { + J.VariableDeclarations mv = super.visitVariableDeclarations(multiVariable, ctx); + if (mv.getVariables().isEmpty() + || mv.getVariables().get(0).getInitializer() == null) { + return mv; + } + + Expression initializer = mv.getVariables().get(0).getInitializer(); + if (initializer instanceof J.MethodInvocation) { + J.MethodInvocation inv = (J.MethodInvocation) initializer; + if (INIT_STANDARD_OBJECTS_MATCHER.matches(inv)) { + J.VariableDeclarations updated = + migrateParameterType(mv, SCRIPTABLE, VAR_SCOPE); + if (updated != null) { + maybeRemoveImport(SCRIPTABLE); + return updated; + } + } + } + return mv; + } + + @Override + public J.Assignment visitAssignment(J.Assignment assignment, ExecutionContext ctx) { + J.Assignment asgn = super.visitAssignment(assignment, ctx); + if (asgn.getAssignment() instanceof J.MethodInvocation) { + J.MethodInvocation inv = (J.MethodInvocation) asgn.getAssignment(); + if (INIT_STANDARD_OBJECTS_MATCHER.matches(inv)) { + maybeRemoveImport(SCRIPTABLE); + } + } + return asgn; + } + }; + } +} diff --git a/rhino-rewrite/src/main/resources/META-INF/rewrite/rhino2.yml b/rhino-rewrite/src/main/resources/META-INF/rewrite/rhino2.yml new file mode 100644 index 0000000000..b308991fc1 --- /dev/null +++ b/rhino-rewrite/src/main/resources/META-INF/rewrite/rhino2.yml @@ -0,0 +1,18 @@ +--- +type: specs.openrewrite.org/v1beta/recipe +name: org.mozilla.javascript.rewrite.MigrateToRhino2 +displayName: Migrate to Rhino 2.0 +description: > + Migrates code that embeds Mozilla Rhino from 1.x to 2.0. + The primary change is that the `scope` parameter in `Callable.call()`, + `Constructable.construct()`, and `IdFunctionCall.execIdCall()` has + changed from `Scriptable` to the new `VarScope` interface. +tags: + - rhino + - javascript + - migration +recipeList: + - org.mozilla.javascript.rewrite.MigrateCallableToVarScope: + migrateScope: true + migrateThisObj: true + migrateScriptExec: true diff --git a/rhino-rewrite/src/test/java/org/mozilla/javascript/rewrite/MigrateCallableToVarScopeTest.java b/rhino-rewrite/src/test/java/org/mozilla/javascript/rewrite/MigrateCallableToVarScopeTest.java new file mode 100644 index 0000000000..05b6e4b3cb --- /dev/null +++ b/rhino-rewrite/src/test/java/org/mozilla/javascript/rewrite/MigrateCallableToVarScopeTest.java @@ -0,0 +1,463 @@ +/* -*- Mode: java; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 4 -*- + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.javascript.rewrite; + +import static org.openrewrite.java.Assertions.java; + +import org.junit.jupiter.api.Test; +import org.openrewrite.InMemoryExecutionContext; +import org.openrewrite.java.JavaParser; +import org.openrewrite.test.RecipeSpec; +import org.openrewrite.test.RewriteTest; +import org.openrewrite.test.TypeValidation; + +/** + * Tests for {@link MigrateCallableToVarScope}. + * + *

Each test provides "before" Java source and "after" Java source. OpenRewrite runs the recipe + * and asserts the result matches the expected "after" state. + * + *

OpenRewrite's isolated Java 17 parser resolves Rhino types from a JAR placed under {@code + * META-INF/rewrite/classpath/} in the test resources, loaded via {@code + * classpathFromResources("rhino")}. The Gradle {@code copyRhinoJarToRewriteClasspath} task + * generates this file at build time. + */ +class MigrateCallableToVarScopeTest implements RewriteTest { + + @Override + public void defaults(RecipeSpec spec) { + spec.recipe(new MigrateCallableToVarScope()) + .typeValidationOptions(TypeValidation.none()) + .parser( + JavaParser.fromJavaVersion() + .classpathFromResources(new InMemoryExecutionContext(), "rhino")); + } + + @Test + void migratesCallableImpl() { + rewriteRun( + java( + "import org.mozilla.javascript.Callable;\n" + + "import org.mozilla.javascript.Context;\n" + + "import org.mozilla.javascript.Scriptable;\n" + + "\n" + + "public class MyCallable implements Callable {\n" + + " @Override\n" + + " public Object call(Context cx, Scriptable scope, Scriptable thisObj," + + " Object[] args) {\n" + + " return null;\n" + + " }\n" + + "}", + "import org.mozilla.javascript.Callable;\n" + + "import org.mozilla.javascript.Context;\n" + + "import org.mozilla.javascript.VarScope;\n" + + "\n" + + "public class MyCallable implements Callable {\n" + + " @Override\n" + + " public Object call(Context cx, VarScope scope, Object thisObj," + + " Object[] args) {\n" + + " return null;\n" + + " }\n" + + "}")); + } + + @Test + void migratesConstructableImpl() { + rewriteRun( + java( + "import org.mozilla.javascript.Constructable;\n" + + "import org.mozilla.javascript.Context;\n" + + "import org.mozilla.javascript.Scriptable;\n" + + "\n" + + "public class MyConstructable implements Constructable {\n" + + " @Override\n" + + " public Scriptable construct(Context cx, Scriptable scope," + + " Object[] args) {\n" + + " return null;\n" + + " }\n" + + "}", + "import org.mozilla.javascript.Constructable;\n" + + "import org.mozilla.javascript.Context;\n" + + "import org.mozilla.javascript.Scriptable;\n" + + "import org.mozilla.javascript.VarScope;\n" + + "\n" + + "public class MyConstructable implements Constructable {\n" + + " @Override\n" + + " public Scriptable construct(Context cx, VarScope scope," + + " Object[] args) {\n" + + " return null;\n" + + " }\n" + + "}")); + } + + @Test + void migratesIdFunctionCallImpl() { + rewriteRun( + java( + "import org.mozilla.javascript.Context;\n" + + "import org.mozilla.javascript.IdFunctionCall;\n" + + "import org.mozilla.javascript.IdFunctionObject;\n" + + "import org.mozilla.javascript.Scriptable;\n" + + "\n" + + "public class MyIdCall implements IdFunctionCall {\n" + + " @Override\n" + + " public Object execIdCall(IdFunctionObject f, Context cx," + + " Scriptable scope, Scriptable thisObj, Object[] args) {\n" + + " return null;\n" + + " }\n" + + "}", + "import org.mozilla.javascript.*;\n" + + "\n" + + "public class MyIdCall implements IdFunctionCall {\n" + + " @Override\n" + + " public Object execIdCall(IdFunctionObject f, Context cx," + + " VarScope scope, Object thisObj, Object[] args) {\n" + + " return null;\n" + + " }\n" + + "}")); + } + + @Test + void doesNotModifyAlreadyMigratedCode() { + rewriteRun( + java( + "import org.mozilla.javascript.Callable;\n" + + "import org.mozilla.javascript.Context;\n" + + "import org.mozilla.javascript.Scriptable;\n" + + "import org.mozilla.javascript.VarScope;\n" + + "\n" + + "public class AlreadyMigrated implements Callable {\n" + + " @Override\n" + + " public Object call(Context cx, VarScope scope, Object thisObj," + + " Object[] args) {\n" + + " return null;\n" + + " }\n" + + "}", + "import org.mozilla.javascript.Callable;\n" + + "import org.mozilla.javascript.Context;\n" + + "import org.mozilla.javascript.VarScope;\n" + + "\n" + + "public class AlreadyMigrated implements Callable {\n" + + " @Override\n" + + " public Object call(Context cx, VarScope scope, Object thisObj," + + " Object[] args) {\n" + + " return null;\n" + + " }\n" + + "}")); + } + + @Test + void doesNotModifyUnrelatedMethods() { + rewriteRun( + java( + "import org.mozilla.javascript.Scriptable;\n" + + "\n" + + "public class Unrelated {\n" + + " public void call(Scriptable scope) {}\n" + + "}")); + } + + @Test + void migratesCallableCallSiteScriptableCast() { + rewriteRun( + spec -> + spec.typeValidationOptions( + TypeValidation.builder().methodInvocations(false).build()), + java( + "import org.mozilla.javascript.Callable;\n" + + "import org.mozilla.javascript.Context;\n" + + "import org.mozilla.javascript.Scriptable;\n" + + "import org.mozilla.javascript.VarScope;\n" + + "\n" + + "public class CallSiteExample {\n" + + " public Object invoke(Callable callable, Context cx, VarScope scope," + + " Scriptable thisObj, Object[] args) {\n" + + " return callable.call(cx, (Scriptable) scope, (Scriptable) thisObj, args);\n" + + " }\n" + + "}", + "import org.mozilla.javascript.Callable;\n" + + "import org.mozilla.javascript.Context;\n" + + "import org.mozilla.javascript.Scriptable;\n" + + "import org.mozilla.javascript.VarScope;\n" + + "\n" + + "public class CallSiteExample {\n" + + " public Object invoke(Callable callable, Context cx, VarScope scope," + + " Scriptable thisObj, Object[] args) {\n" + + " return callable.call(cx, (VarScope) scope, (Object) thisObj, args);\n" + + " }\n" + + "}")); + } + + @Test + void migratesCallableLambdaExplicitlyTyped() { + rewriteRun( + java( + "import org.mozilla.javascript.Callable;\n" + + "import org.mozilla.javascript.Context;\n" + + "import org.mozilla.javascript.Scriptable;\n" + + "\n" + + "public class Test {\n" + + " Callable c = (Context cx, Scriptable scope, Scriptable thisObj, Object[] args) -> null;\n" + + "}", + "import org.mozilla.javascript.Callable;\n" + + "import org.mozilla.javascript.Context;\n" + + "import org.mozilla.javascript.Scriptable;\n" + + "import org.mozilla.javascript.VarScope;\n" + + "\n" + + "public class Test {\n" + + " Callable c = (Context cx, VarScope scope, Object thisObj, Object[] args) -> null;\n" + + "}")); + } + + @Test + void migratesConstructableLambdaExplicitlyTyped() { + rewriteRun( + java( + "import org.mozilla.javascript.Constructable;\n" + + "import org.mozilla.javascript.Context;\n" + + "import org.mozilla.javascript.Scriptable;\n" + + "\n" + + "public class Test {\n" + + " Constructable c = (Context cx, Scriptable scope, Object[] args) -> null;\n" + + "}", + "import org.mozilla.javascript.Constructable;\n" + + "import org.mozilla.javascript.Context;\n" + + "import org.mozilla.javascript.Scriptable;\n" + + "import org.mozilla.javascript.VarScope;\n" + + "\n" + + "public class Test {\n" + + " Constructable c = (Context cx, VarScope scope, Object[] args) -> null;\n" + + "}")); + } + + @Test + void ignoresImplicitlyTypedCallableLambda() { + rewriteRun( + java( + "import org.mozilla.javascript.Callable;\n" + + "\n" + + "public class Test {\n" + + " Callable c = (cx, scope, thisObj, args) -> null;\n" + + "}")); + } + + @Test + void migratesLambdaFunctionTarget() { + rewriteRun( + java( + "import org.mozilla.javascript.Context;\n" + + "import org.mozilla.javascript.LambdaFunction;\n" + + "import org.mozilla.javascript.Scriptable;\n" + + "\n" + + "public class Test {\n" + + " public void make(Scriptable parent) {\n" + + " new LambdaFunction(parent, \"foo\", 1, (Context cx, Scriptable scope, Scriptable thisObj, Object[] args) -> null);\n" + + " }\n" + + "}", + "import org.mozilla.javascript.Context;\n" + + "import org.mozilla.javascript.LambdaFunction;\n" + + "import org.mozilla.javascript.Scriptable;\n" + + "import org.mozilla.javascript.VarScope;\n" + + "\n" + + "public class Test {\n" + + " public void make(Scriptable parent) {\n" + + " new LambdaFunction(parent, \"foo\", 1, (Context cx, VarScope scope, Object thisObj, Object[] args) -> null);\n" + + " }\n" + + "}")); + } + + @Test + void migratesLambdaConstructorTarget() { + rewriteRun( + java( + "import org.mozilla.javascript.Context;\n" + + "import org.mozilla.javascript.LambdaConstructor;\n" + + "import org.mozilla.javascript.Scriptable;\n" + + "\n" + + "public class Test {\n" + + " public void make(Scriptable parent) {\n" + + " new LambdaConstructor(parent, \"foo\", 1, (Context cx, Scriptable scope, Object[] args) -> null);\n" + + " }\n" + + "}", + "import org.mozilla.javascript.Context;\n" + + "import org.mozilla.javascript.LambdaConstructor;\n" + + "import org.mozilla.javascript.Scriptable;\n" + + "import org.mozilla.javascript.VarScope;\n" + + "\n" + + "public class Test {\n" + + " public void make(Scriptable parent) {\n" + + " new LambdaConstructor(parent, \"foo\", 1, (Context cx, VarScope scope, Object[] args) -> null);\n" + + " }\n" + + "}")); + } + + @Test + void migratesAnonymousClass() { + rewriteRun( + java( + "import org.mozilla.javascript.Callable;\n" + + "import org.mozilla.javascript.Context;\n" + + "import org.mozilla.javascript.Scriptable;\n" + + "\n" + + "public class Test {\n" + + " Callable c = new Callable() {\n" + + " @Override\n" + + " public Object call(Context cx, Scriptable scope, Scriptable thisObj, Object[] args) {\n" + + " return null;\n" + + " }\n" + + " };\n" + + "}", + "import org.mozilla.javascript.Callable;\n" + + "import org.mozilla.javascript.Context;\n" + + "import org.mozilla.javascript.VarScope;\n" + + "\n" + + "public class Test {\n" + + " Callable c = new Callable() {\n" + + " @Override\n" + + " public Object call(Context cx, VarScope scope, Object thisObj, Object[] args) {\n" + + " return null;\n" + + " }\n" + + " };\n" + + "}")); + } + + @Test + void migratesScriptExec() { + rewriteRun( + java( + "import org.mozilla.javascript.Context;\n" + + "import org.mozilla.javascript.Script;\n" + + "import org.mozilla.javascript.Scriptable;\n" + + "\n" + + "public class MyScript implements Script {\n" + + " @Override\n" + + " public Object exec(Context cx, Scriptable scope, Scriptable thisObj) {\n" + + " return null;\n" + + " }\n" + + "}", + "import org.mozilla.javascript.Context;\n" + + "import org.mozilla.javascript.Script;\n" + + "import org.mozilla.javascript.VarScope;\n" + + "\n" + + "public class MyScript implements Script {\n" + + " @Override\n" + + " public Object exec(Context cx, VarScope scope, Object thisObj) {\n" + + " return null;\n" + + " }\n" + + "}")); + } + + @Test + void migratesScriptExec2Param() { + rewriteRun( + java( + "import org.mozilla.javascript.Context;\n" + + "import org.mozilla.javascript.Script;\n" + + "import org.mozilla.javascript.Scriptable;\n" + + "\n" + + "public class MyScript implements Script {\n" + + " @Override\n" + + " public Object exec(Context cx, Scriptable scope) {\n" + + " return null;\n" + + " }\n" + + "}", + "import org.mozilla.javascript.Context;\n" + + "import org.mozilla.javascript.Script;\n" + + "import org.mozilla.javascript.VarScope;\n" + + "\n" + + "public class MyScript implements Script {\n" + + " @Override\n" + + " public Object exec(Context cx, VarScope scope) {\n" + + " return null;\n" + + " }\n" + + "}")); + } + + @Test + void migratesInitStandardObjectsResult() { + rewriteRun( + java( + "import org.mozilla.javascript.Context;\n" + + "import org.mozilla.javascript.Scriptable;\n" + + "\n" + + "public class Test {\n" + + " public void init() {\n" + + " Context cx = Context.enter();\n" + + " Scriptable scope = cx.initStandardObjects();\n" + + " }\n" + + "}", + "import org.mozilla.javascript.Context;\n" + + "import org.mozilla.javascript.VarScope;\n" + + "\n" + + "public class Test {\n" + + " public void init() {\n" + + " Context cx = Context.enter();\n" + + " VarScope scope = cx.initStandardObjects();\n" + + " }\n" + + "}")); + } + + @Test + void updatesJavadoc() { + rewriteRun( + java( + "import org.mozilla.javascript.Callable;\n" + + "import org.mozilla.javascript.Context;\n" + + "import org.mozilla.javascript.Scriptable;\n" + + "\n" + + "public class MyCallable implements Callable {\n" + + " /**\n" + + " * @param scope the Scriptable scope\n" + + " * @param thisObj the Scriptable thisObj\n" + + " */\n" + + " @Override\n" + + " public Object call(Context cx, Scriptable scope, Scriptable thisObj, Object[] args) {\n" + + " return null;\n" + + " }\n" + + "}", + "import org.mozilla.javascript.Callable;\n" + + "import org.mozilla.javascript.Context;\n" + + "import org.mozilla.javascript.VarScope;\n" + + "\n" + + "public class MyCallable implements Callable {\n" + + " /**\n" + + " * @param scope the VarScope scope\n" + + " * @param thisObj the Object thisObj\n" + + " */\n" + + " @Override\n" + + " public Object call(Context cx, VarScope scope, Object thisObj, Object[] args) {\n" + + " return null;\n" + + " }\n" + + "}")); + } + + @Test + void removesUnusedScriptableImport() { + rewriteRun( + java( + "import org.mozilla.javascript.Callable;\n" + + "import org.mozilla.javascript.Context;\n" + + "import org.mozilla.javascript.Scriptable;\n" + + "\n" + + "public class MyCallable implements Callable {\n" + + " @Override\n" + + " public Object call(Context cx, Scriptable scope, Scriptable thisObj, Object[] args) {\n" + + " return null;\n" + + " }\n" + + "}", + "import org.mozilla.javascript.Callable;\n" + + "import org.mozilla.javascript.Context;\n" + + "import org.mozilla.javascript.VarScope;\n" + + "\n" + + "public class MyCallable implements Callable {\n" + + " @Override\n" + + " public Object call(Context cx, VarScope scope, Object thisObj, Object[] args) {\n" + + " return null;\n" + + " }\n" + + "}")); + } +} diff --git a/settings.gradle b/settings.gradle index 252ad5d242..ab08103683 100644 --- a/settings.gradle +++ b/settings.gradle @@ -3,6 +3,6 @@ plugins { id 'org.jetbrains.kotlin.jvm' version '2.3.20' apply false } rootProject.name = 'rhino-root' -include 'rhino', 'rhino-engine', 'rhino-tools', 'rhino-xml', 'rhino-all', 'examples', 'testutils', 'tests', 'benchmarks' +include 'rhino', 'rhino-engine', 'rhino-tools', 'rhino-xml', 'rhino-all', 'rhino-rewrite', 'examples', 'testutils', 'tests', 'benchmarks' include 'rhino-kotlin' include 'it-android'