diff --git a/build-tools-internal/build.gradle b/build-tools-internal/build.gradle index 66780fc36e3b..4735b5d08312 100644 --- a/build-tools-internal/build.gradle +++ b/build-tools-internal/build.gradle @@ -123,6 +123,10 @@ gradlePlugin { id = 'elasticsearch.rest-test' implementationClass = 'org.elasticsearch.gradle.internal.test.RestTestPlugin' } + rewrite { + id = 'elasticsearch.rewrite' + implementationClass = 'org.elasticsearch.gradle.internal.rewrite.RewritePlugin' + } standaloneRestTest { id = 'elasticsearch.standalone-rest-test' implementationClass = 'org.elasticsearch.gradle.internal.test.StandaloneRestTestPlugin' @@ -197,13 +201,15 @@ repositories { configurations { integTestRuntimeOnly.extendsFrom(testRuntimeOnly) } + dependencies { api localGroovy() - + api gradleApi() api "org.elasticsearch:build-conventions:$version" api "org.elasticsearch.gradle:build-tools:$version" - + api "org.openrewrite:rewrite-java:7.10.0" + api 'org.openrewrite:plugin:5.6.0' api 'commons-codec:commons-codec:1.12' api 'org.apache.commons:commons-compress:1.19' api 'org.apache.ant:ant:1.10.8' diff --git a/build-tools-internal/src/integTest/groovy/org/elasticsearch/gradle/internal/rewrite/AutobackportFuncTest.groovy b/build-tools-internal/src/integTest/groovy/org/elasticsearch/gradle/internal/rewrite/AutobackportFuncTest.groovy new file mode 100644 index 000000000000..911f9aebdde6 --- /dev/null +++ b/build-tools-internal/src/integTest/groovy/org/elasticsearch/gradle/internal/rewrite/AutobackportFuncTest.groovy @@ -0,0 +1,263 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +package org.elasticsearch.gradle.internal.rewrite; + +import org.elasticsearch.gradle.fixtures.AbstractGradleFuncTest + +class AutobackportFuncTest extends AbstractGradleFuncTest { + + def setup() { + file("src/main/java/org/elasticsearch/core/List.java") << """ + package org.elasticsearch.core; + public class List { + public static java.util.List of(T... entries) { + //impl does not matter + return new java.util.ArrayList(); + } + } + """ + file("src/main/java/org/elasticsearch/core/Set.java") << """ + package org.elasticsearch.core; + public class Set { + public static java.util.Set of(T... entries) { + //impl does not matter + return new java.util.HashSet(); + } + } + """ + } + + def "can run rewrite to backport java util methods"() { + when: + setupRewriteYamlConfig() + def sourceFile = file("src/main/java/org/acme/SomeClass.java") + sourceFile << """ +package org.acme; + +import java.util.List; +import java.util.Map; +import java.util.Set; + +class SomeClass { + public void someMethod() { + List myList = List.of("some", "non", "java8", "code"); + Set mySet = Set.of("some", "non", "java8", "code"); + Map myMap = Map.of(List.of("some", "non"), Set.of("java8", "code")); + } +} +""" + + buildFile.text = """ + plugins { + id 'java' + id 'elasticsearch.rewrite' + } + rewrite { + rewriteVersion = "7.11.0" + activeRecipe("org.elasticsearch.java.backport.ListOfBackport", + "org.elasticsearch.java.backport.MapOfBackport", + "org.elasticsearch.java.backport.SetOfBackport") + configFile = rootProject.file("rewrite.yml") + } + + repositories { + mavenCentral() + } + + dependencies { + rewrite "org.openrewrite:rewrite-java-11" + rewrite "org.openrewrite:rewrite-java" + } + """ + + then: + gradleRunner("rewrite", '--stacktrace').build() + + sourceFile.text == """ +package org.acme; + +import java.util.List; +import java.util.Map; +import java.util.Set; + +class SomeClass { + public void someMethod() { + List myList = org.elasticsearch.core.List.of("some", "non", "java8", "code"); + Set mySet = org.elasticsearch.core.Set.of("some", "non", "java8", "code"); + Map myMap = org.elasticsearch.core.Map.of(org.elasticsearch.core.List.of("some", "non"), org.elasticsearch.core.Set.of("java8", "code")); + } +} +""" + } + + def "converts new full qualified usage of elastic util methods where possible"() { + when: + setupRewriteYamlConfig() + def sourceFile = file("src/main/java/org/acme/SomeClass.java") + sourceFile << """ +package org.acme; + +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.Set; + +class SomeClass { + public void someMethod() { + Collection myList = List.of("some", "non", "java8", "code"); + Collection mySet = Set.of("some", "non", "java8", "code"); + Map myMap = Map.of(List.of("some", "non"), Set.of("java8", "code")); + } +} +""" + + buildFile.text = """ + plugins { + id 'java' + id 'elasticsearch.rewrite' + } + rewrite { + rewriteVersion = "7.10.0" + activeRecipe("org.elasticsearch.java.backport.ListOfBackport", + "org.elasticsearch.java.backport.MapOfBackport", + "org.elasticsearch.java.backport.SetOfBackport") + configFile = rootProject.file("rewrite.yml") + } + + repositories { + mavenCentral() + } + + dependencies { + rewrite "org.openrewrite:rewrite-java-11" + rewrite "org.openrewrite:rewrite-java" + } + """ + + then: + gradleRunner("rewrite", '--stacktrace').build() + + sourceFile.text == """ +package org.acme; + +import org.elasticsearch.core.List; +import org.elasticsearch.core.Set; + +import java.util.Collection; +import java.util.Map; + +class SomeClass { + public void someMethod() { + Collection myList = List.of("some", "non", "java8", "code"); + Collection mySet = Set.of("some", "non", "java8", "code"); + Map myMap = org.elasticsearch.core.Map.of(List.of("some", "non"), Set.of("java8", "code")); + } +} +""" + } + + def "compatible code is not changed"() { + when: + setupRewriteYamlConfig() + def sourceFile = file("src/main/java/org/acme/SomeClass.java") + sourceFile << """ +package org.acme; + +import java.util.Collection; + +class SomeClass { + public void someMethod() { + Collection myList = org.elasticsearch.core.List.of("some", "non", "java8", "code"); + Collection mySet = org.elasticsearch.core.Set.of("some", "non", "java8", "code"); + } +} +""" + + buildFile.text = """ + plugins { + id 'java' + id 'elasticsearch.rewrite' + } + rewrite { + rewriteVersion = "7.10.0" + activeRecipe("org.elasticsearch.java.backport.ListOfBackport", + "org.elasticsearch.java.backport.MapOfBackport", + "org.elasticsearch.java.backport.SetOfBackport") + configFile = rootProject.file("rewrite.yml") + } + + repositories { + mavenCentral() + maven { url 'https://jitpack.io' } + } + + dependencies { + rewrite "org.openrewrite:rewrite-java-11" + rewrite "org.openrewrite:rewrite-java" + } + + """ + + then: + gradleRunner("rewrite", '--stacktrace').build() + + sourceFile.text == """ +package org.acme; + +import java.util.Collection; + +class SomeClass { + public void someMethod() { + Collection myList = org.elasticsearch.core.List.of("some", "non", "java8", "code"); + Collection mySet = org.elasticsearch.core.Set.of("some", "non", "java8", "code"); + } +} +""" + } + + private File setupRewriteYamlConfig() { + file("rewrite.yml") << """ +type: specs.openrewrite.org/v1beta/recipe +name: org.elasticsearch.java.backport.ListOfBackport +displayName: Use `org.elasticsearch.core.Lists#of(..)` not java.util.List.of#(..) +description: Java 8 does not support the `java.util.List#of(..)`. +tags: + - backport +recipeList: + - org.elasticsearch.gradle.internal.rewrite.rules.ChangeMethodOwnerRecipe: + originFullQualifiedClassname: java.util.List + targetFullQualifiedClassname: org.elasticsearch.core.List + methodName: of +--- +type: specs.openrewrite.org/v1beta/recipe +name: org.elasticsearch.java.backport.MapOfBackport +displayName: Use `org.elasticsearch.core.Maps#of(..)` not java.util.Map.of#(..) +description: Java 8 does not support the `java.util.Map#of(..)`. +tags: + - backport +recipeList: + - org.elasticsearch.gradle.internal.rewrite.rules.ChangeMethodOwnerRecipe: + originFullQualifiedClassname: java.util.Map + targetFullQualifiedClassname: org.elasticsearch.core.Map + methodName: of +--- +type: specs.openrewrite.org/v1beta/recipe +name: org.elasticsearch.java.backport.SetOfBackport +displayName: Use `org.elasticsearch.core.Sets#of(..)` not java.util.Set.of#(..) +description: Java 8 does not support the `java.util.Set#of(..)`. +tags: + - backport +recipeList: + - org.elasticsearch.gradle.internal.rewrite.rules.ChangeMethodOwnerRecipe: + originFullQualifiedClassname: java.util.Set + targetFullQualifiedClassname: org.elasticsearch.core.Set + methodName: of +""" + } +} diff --git a/build-tools-internal/src/main/groovy/elasticsearch.auto-backporting.gradle b/build-tools-internal/src/main/groovy/elasticsearch.auto-backporting.gradle new file mode 100644 index 000000000000..5600364bb28c --- /dev/null +++ b/build-tools-internal/src/main/groovy/elasticsearch.auto-backporting.gradle @@ -0,0 +1,55 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + + +import org.elasticsearch.gradle.internal.rewrite.RewritePlugin + +allprojects { + apply plugin: 'elasticsearch.rewrite' + rewrite { + rewriteVersion = "7.11.0" + activeRecipe("org.elasticsearch.java.backport.ListOfBackport", + "org.elasticsearch.java.backport.MapOfBackport", + "org.elasticsearch.java.backport.SetOfBackport") + configFile = rootProject.file("rewrite.yml") + } + + repositories { + mavenCentral() + } + + configurations { + rewrite { + resolutionStrategy { + force 'org.jetbrains:annotations:21.0.1' + force 'org.slf4j:slf4j-api:1.7.31' + force 'org.jetbrains.kotlin:kotlin-stdlib:1.5.10' + force 'org.jetbrains.kotlin:kotlin-stdlib-common:1.5.10' + } + } + } + + dependencies { + rewrite "org.openrewrite:rewrite-java-11" + rewrite "org.openrewrite:rewrite-java" + } + + if (gradle.getStartParameter().getTaskNames().any { it.endsWith(RewritePlugin.REWRITE_TASKNAME) }) { + afterEvaluate { + def java = project.getExtensions().findByType(JavaPluginExtension.class); + if (java) { + java.setSourceCompatibility(JavaVersion.VERSION_11) + java.setTargetCompatibility(JavaVersion.VERSION_11) + } + } + } + + tasks.named('rewrite').configure {rewrite -> + rewrite.getMaxHeapSize().set("2g") + } +} diff --git a/build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/rewrite/RewriteExtension.java b/build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/rewrite/RewriteExtension.java new file mode 100644 index 000000000000..7b0c9139c5ac --- /dev/null +++ b/build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/rewrite/RewriteExtension.java @@ -0,0 +1,51 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +package org.elasticsearch.gradle.internal.rewrite; + +import java.io.File; +import java.util.ArrayList; +import java.util.List; + +import static java.util.Arrays.asList; + +public class RewriteExtension { + private final List activeRecipes = new ArrayList<>(); + private File configFile; + private String rewriteVersion = "7.10.0"; + + public void setConfigFile(File configFile) { + this.configFile = configFile; + } + + public File getConfigFile() { + return configFile; + } + + public void activeRecipe(String... recipes) { + activeRecipes.addAll(asList(recipes)); + } + + public void setActiveRecipes(List activeRecipes) { + this.activeRecipes.clear(); + this.activeRecipes.addAll(activeRecipes); + } + + public List getActiveRecipes() { + return activeRecipes; + } + + public String getRewriteVersion() { + return rewriteVersion; + } + + public void setRewriteVersion(String value) { + rewriteVersion = value; + } + +} diff --git a/build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/rewrite/RewriteParameters.java b/build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/rewrite/RewriteParameters.java new file mode 100644 index 000000000000..2e780ea75c24 --- /dev/null +++ b/build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/rewrite/RewriteParameters.java @@ -0,0 +1,26 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +package org.elasticsearch.gradle.internal.rewrite; + +import org.gradle.api.file.RegularFileProperty; +import org.gradle.api.provider.ListProperty; +import org.gradle.workers.WorkParameters; + +import java.io.File; + +public interface RewriteParameters extends WorkParameters { + + ListProperty getActiveRecipes(); + ListProperty getActiveStyles(); + + ListProperty getAllJavaPaths(); + ListProperty getAllDependencyPaths(); + RegularFileProperty getProjectDirectory(); + RegularFileProperty getConfigFile(); +} diff --git a/build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/rewrite/RewritePlugin.java b/build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/rewrite/RewritePlugin.java new file mode 100644 index 000000000000..4539da380d0a --- /dev/null +++ b/build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/rewrite/RewritePlugin.java @@ -0,0 +1,64 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +package org.elasticsearch.gradle.internal.rewrite; + +import org.gradle.api.Action; +import org.gradle.api.Plugin; +import org.gradle.api.Project; + +import org.gradle.api.artifacts.Configuration; +import org.gradle.api.artifacts.DependencyResolveDetails; +import org.gradle.api.artifacts.ModuleVersionSelector; +import org.gradle.api.file.ProjectLayout; +import org.gradle.api.plugins.JavaBasePlugin; +import org.gradle.api.plugins.JavaPluginExtension; +import org.gradle.api.provider.ProviderFactory; + +import javax.inject.Inject; + +/** + * Adds the RewriteExtension to the current project and registers tasks per-sourceSet. + * Only needs to be applied to projects with java sources. No point in applying this to any project that does + * not have java sources of its own, such as the root project in a multi-project builds. + */ +public class RewritePlugin implements Plugin { + + public static final String REWRITE_TASKNAME = "rewrite"; + private ProviderFactory providerFactory; + private ProjectLayout projectLayout; + + @Inject + public RewritePlugin(ProviderFactory providerFactory, ProjectLayout projectLayout) { + this.providerFactory = providerFactory; + this.projectLayout = projectLayout; + } + + @Override + public void apply(Project project) { + final RewriteExtension extension = project.getExtensions().create("rewrite", RewriteExtension.class); + // Rewrite module dependencies put here will be available to all rewrite tasks + Configuration rewriteConf = project.getConfigurations().maybeCreate("rewrite"); + rewriteConf.getResolutionStrategy().eachDependency(details -> { + ModuleVersionSelector requested = details.getRequested(); + if (requested.getGroup().equals("org.openrewrite") && requested.getVersion().isBlank() || requested.getVersion() == null) { + details.useVersion(extension.getRewriteVersion()); + } + }); + RewriteTask rewriteTask = project.getTasks().create(REWRITE_TASKNAME, RewriteTask.class, rewriteConf, extension); + rewriteTask.getActiveRecipes().convention(providerFactory.provider(() -> extension.getActiveRecipes())); + rewriteTask.getConfigFile().convention(projectLayout.file(providerFactory.provider(() -> extension.getConfigFile()))); + project.getPlugins().withType(JavaBasePlugin.class, javaBasePlugin -> { + JavaPluginExtension javaPluginExtension = project.getExtensions().getByType(JavaPluginExtension.class); + javaPluginExtension.getSourceSets().all( sourceSet -> { + rewriteTask.getSourceFiles().from(sourceSet.getAllSource()); + rewriteTask.getDependencyFiles().from(sourceSet.getCompileClasspath()); + }); + }); + } +} diff --git a/build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/rewrite/RewriteReflectiveFacade.java b/build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/rewrite/RewriteReflectiveFacade.java new file mode 100644 index 000000000000..eff630139eda --- /dev/null +++ b/build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/rewrite/RewriteReflectiveFacade.java @@ -0,0 +1,618 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +package org.elasticsearch.gradle.internal.rewrite; + +import javax.annotation.Nullable; +import java.io.InputStream; +import java.net.URI; +import java.nio.charset.Charset; +import java.nio.file.Path; +import java.util.Collection; +import java.util.List; +import java.util.Objects; +import java.util.Properties; +import java.util.Set; +import java.util.function.Consumer; +import java.util.stream.Collectors; + +import static java.util.stream.Collectors.toList; + +/** + * Provides access to Rewrite classes resolved and loaded from the supplied dependency configuration. + */ +@SuppressWarnings({"unchecked", "UnusedReturnValue", "InnerClassMayBeStatic"}) +public class RewriteReflectiveFacade { + + private ClassLoader getClassLoader() { + return getClass().getClassLoader(); + } + + private List parseBase(Object real, Iterable sourcePaths, Path baseDir, RewriteReflectiveFacade.InMemoryExecutionContext ctx) { + try { + Class executionContextClass = getClass().getClassLoader().loadClass("org.openrewrite.ExecutionContext"); + List results = (List) real.getClass() + .getMethod("parse", Iterable.class, Path.class, executionContextClass) + .invoke(real, sourcePaths, baseDir, ctx.real); + return results.stream() + .map(RewriteReflectiveFacade.SourceFile::new) + .collect(toList()); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + public class EnvironmentBuilder { + private final Object real; + + private EnvironmentBuilder(Object real) { + this.real = real; + } + + public RewriteReflectiveFacade.EnvironmentBuilder scanRuntimeClasspath(String... acceptPackages) { + try { + real.getClass().getMethod("scanRuntimeClasspath", String[].class).invoke(real, new Object[]{acceptPackages}); + } catch (Exception e) { + throw new RuntimeException(e); + } + return this; + } + + public RewriteReflectiveFacade.EnvironmentBuilder scanJar(Path jar, ClassLoader classLoader) { + try { + real.getClass().getMethod("scanJar", Path.class, ClassLoader.class).invoke(real, jar, classLoader); + } catch (Exception e) { + throw new RuntimeException(e); + } + return this; + } + + public RewriteReflectiveFacade.EnvironmentBuilder scanJar(Path jar) { + try { + real.getClass().getMethod("scanJar", Path.class, ClassLoader.class).invoke(real, jar, getClassLoader()); + } catch (Exception e) { + throw new RuntimeException(e); + } + return this; + } + + public RewriteReflectiveFacade.EnvironmentBuilder scanUserHome() { + try { + real.getClass().getMethod("scanUserHome").invoke(real); + } catch (Exception e) { + throw new RuntimeException(e); + } + return this; + } + + public RewriteReflectiveFacade.EnvironmentBuilder load(RewriteReflectiveFacade.YamlResourceLoader yamlResourceLoader) { + try { + Class resourceLoaderClass = getClassLoader().loadClass("org.openrewrite.config.ResourceLoader"); + real.getClass().getMethod("load", resourceLoaderClass).invoke(real, yamlResourceLoader.real); + } catch (Exception e) { + throw new RuntimeException(e); + } + return this; + } + + public RewriteReflectiveFacade.Environment build() { + try { + return new RewriteReflectiveFacade.Environment(real.getClass().getMethod("build").invoke(real)); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + } + + public class SourceFile { + private final Object real; + + private SourceFile(Object real) { + this.real = real; + } + + public Path getSourcePath() { + try { + return (Path) real.getClass().getMethod("getSourcePath").invoke(real); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + public String print() { + try { + return (String) real.getClass().getMethod("print").invoke(real); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + } + + public class Result { + private final Object real; + + private Result(Object real) { + this.real = real; + } + + @Nullable + public RewriteReflectiveFacade.SourceFile getBefore() { + try { + return new RewriteReflectiveFacade.SourceFile(real.getClass().getMethod("getBefore").invoke(real)); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + @Nullable + public RewriteReflectiveFacade.SourceFile getAfter() { + try { + return new RewriteReflectiveFacade.SourceFile(real.getClass().getMethod("getAfter").invoke(real)); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + public List getRecipesThatMadeChanges() { + try { + Set result = (Set) real.getClass().getMethod("getRecipesThatMadeChanges").invoke(real); + return result.stream() + .map(RewriteReflectiveFacade.Recipe::new) + .collect(Collectors.toList()); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + public String diff() { + try { + return (String) real.getClass().getMethod("diff").invoke(real); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + } + + public class Recipe { + private final Object real; + + private Recipe(Object real) { + this.real = real; + } + + public List run(List sources) { + try { + List unwrappedSources = sources.stream().map(it -> it.real).collect(toList()); + List result = (List) real.getClass().getMethod("run", List.class) + .invoke(real, unwrappedSources); + return result.stream() + .map(RewriteReflectiveFacade.Result::new) + .collect(toList()); + } catch (Exception e) { + e.printStackTrace(); + throw new RuntimeException(e); + } + } + + public List run(List sources, + RewriteReflectiveFacade.InMemoryExecutionContext ctx) { + try { + Class executionContextClass = getClassLoader().loadClass("org.openrewrite.ExecutionContext"); + List unwrappedSources = sources.stream().map(it -> it.real).collect(toList()); + List result = (List) real.getClass().getMethod("run", List.class, executionContextClass) + .invoke(real, unwrappedSources, ctx.real); + return result.stream() + .map(RewriteReflectiveFacade.Result::new) + .collect(toList()); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + public String getName() { + try { + return (String) real.getClass().getMethod("getName").invoke(real); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + public Collection validateAll() { + try { + List results = (List) real.getClass().getMethod("validateAll").invoke(real); + return results.stream().map(r -> { + String canonicalName = r.getClass().getCanonicalName(); + if (canonicalName.equals("org.openrewrite.Validated.Invalid")) { + return new RewriteReflectiveFacade.Validated.Invalid(r); + } else if (canonicalName.equals("org.openrewrite.Validated.Both")) { + return new RewriteReflectiveFacade.Validated.Both(r); + } else { + return null; + } + }).filter(Objects::nonNull).collect(toList()); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + } + + public interface Validated { + + Object getReal(); + + default List failures() { + try { + Object real = getReal(); + List results = (List) real.getClass().getMethod("failures").invoke(real); + return results.stream().map(RewriteReflectiveFacade.Validated.Invalid::new).collect(toList()); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + class Invalid implements RewriteReflectiveFacade.Validated { + + private final Object real; + + public Invalid(Object real) { + this.real = real; + } + + @Override + public Object getReal() { + return real; + } + + public String getProperty() { + try { + return (String) real.getClass().getMethod("getProperty").invoke(real); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + public String getMessage() { + try { + return (String) real.getClass().getMethod("getMessage").invoke(real); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + public Throwable getException() { + try { + return (Throwable) real.getClass().getMethod("getException").invoke(real); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + } + + class Both implements RewriteReflectiveFacade.Validated { + + private final Object real; + + public Both(Object real) { + this.real = real; + } + + @Override + public Object getReal() { + return real; + } + } + } + + public class NamedStyles { + private final Object real; + + private NamedStyles(Object real) { + this.real = real; + } + + public String getName() { + try { + return (String) real.getClass().getMethod("getName").invoke(real); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + } + + public class Environment { + private final Object real; + + private Environment(Object real) { + this.real = real; + } + + public List activateStyles(Iterable activeStyles) { + try { + //noinspection unchecked + List raw = (List) real.getClass() + .getMethod("activateStyles", Iterable.class) + .invoke(real, activeStyles); + return raw.stream() + .map(RewriteReflectiveFacade.NamedStyles::new) + .collect(toList()); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + public RewriteReflectiveFacade.Recipe activateRecipes(Iterable activeRecipes) { + try { + return new RewriteReflectiveFacade.Recipe(real.getClass() + .getMethod("activateRecipes", Iterable.class) + .invoke(real, activeRecipes)); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + public Collection listRecipeDescriptors() { + try { + Collection result = (Collection) real.getClass().getMethod("listRecipeDescriptors").invoke(real); + return result.stream() + .map(RewriteReflectiveFacade.RecipeDescriptor::new) + .collect(toList()); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + public Collection listStyles() { + try { + List raw = (List) real.getClass().getMethod("listStyles").invoke(real); + return raw.stream() + .map(RewriteReflectiveFacade.NamedStyles::new) + .collect(toList()); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + } + + public class RecipeDescriptor { + private final Object real; + + private RecipeDescriptor(Object real) { + this.real = real; + } + + public String getName() { + try { + return (String) real.getClass().getMethod("getName").invoke(real); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + public String getDisplayName() { + try { + return (String) real.getClass().getMethod("getDisplayName").invoke(real); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + public String getDescription() { + try { + return (String) real.getClass().getMethod("getDescription").invoke(real); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + public List getOptions() { + try { + List results = (List) real.getClass().getMethod("getOptions").invoke(real); + return results.stream().map(RewriteReflectiveFacade.OptionDescriptor::new).collect(toList()); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + } + + public class OptionDescriptor { + private final Object real; + + private OptionDescriptor(Object real) { + this.real = real; + } + + public String getName() { + try { + return (String) real.getClass().getMethod("getName").invoke(real); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + public String getDisplayName() { + try { + return (String) real.getClass().getMethod("getDisplayName").invoke(real); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + public String getDescription() { + try { + return (String) real.getClass().getMethod("getDescription").invoke(real); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + public String getType() { + try { + return (String) real.getClass().getMethod("getType").invoke(real); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + public String getExample() { + try { + return (String) real.getClass().getMethod("getExample").invoke(real); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + public boolean isRequired() { + try { + return (boolean) real.getClass().getMethod("isRequired").invoke(real); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + } + + public RewriteReflectiveFacade.EnvironmentBuilder environmentBuilder(Properties properties) { + try { + return new RewriteReflectiveFacade.EnvironmentBuilder(getClassLoader() + .loadClass("org.openrewrite.config.Environment") + .getMethod("builder", Properties.class) + .invoke(null, properties) + ); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + public class YamlResourceLoader { + private final Object real; + + private YamlResourceLoader(Object real) { + this.real = real; + } + } + + public RewriteReflectiveFacade.YamlResourceLoader yamlResourceLoader(InputStream yamlInput, URI source, Properties properties) { + try { + return new RewriteReflectiveFacade.YamlResourceLoader(getClassLoader() + .loadClass("org.openrewrite.config.YamlResourceLoader") + .getConstructor(InputStream.class, URI.class, Properties.class) + .newInstance(yamlInput, source, properties) + ); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + public class InMemoryExecutionContext { + private final Object real; + + private InMemoryExecutionContext(Object real) { + this.real = real; + } + } + + public RewriteReflectiveFacade.InMemoryExecutionContext inMemoryExecutionContext(Consumer onError) { + try { + return new RewriteReflectiveFacade.InMemoryExecutionContext(getClassLoader() + .loadClass("org.openrewrite.InMemoryExecutionContext") + .getConstructor(Consumer.class) + .newInstance(onError)); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + public class JavaParserBuilder { + private final Object real; + + private JavaParserBuilder(Object real) { + this.real = real; + } + + public RewriteReflectiveFacade.JavaParserBuilder styles(List styles) { + try { + List unwrappedStyles = styles.stream() + .map(it -> it.real) + .collect(toList()); + real.getClass().getMethod("styles", Iterable.class).invoke(real, unwrappedStyles); + return this; + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + public RewriteReflectiveFacade.JavaParserBuilder classpath(Collection classpath) { + try { + real.getClass().getMethod("classpath", Collection.class).invoke(real, classpath); + return this; + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + public RewriteReflectiveFacade.JavaParserBuilder charset(Charset charset) { + try { + real.getClass().getMethod("charset", Charset.class).invoke(real, charset); + return this; + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + public RewriteReflectiveFacade.JavaParserBuilder logCompilationWarningsAndErrors(boolean logCompilationWarningsAndErrors) { + try { + real.getClass().getMethod("logCompilationWarningsAndErrors", boolean.class).invoke(real, logCompilationWarningsAndErrors); + return this; + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + public RewriteReflectiveFacade.JavaParserBuilder relaxedClassTypeMatching(boolean relaxedClassTypeMatching) { + try { + real.getClass().getMethod("relaxedClassTypeMatching", boolean.class).invoke(real, relaxedClassTypeMatching); + return this; + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + public RewriteReflectiveFacade.JavaParser build() { + try { + return new RewriteReflectiveFacade.JavaParser(real.getClass().getMethod("build").invoke(real)); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + } + + public class JavaParser { + private final Object real; + + private JavaParser(Object real) { + this.real = real; + } + + public List parse(Iterable sourcePaths, Path baseDir, RewriteReflectiveFacade.InMemoryExecutionContext ctx) { + return parseBase(real, sourcePaths, baseDir, ctx); + } + } + + public RewriteReflectiveFacade.JavaParserBuilder javaParserFromJavaVersion() { + try { + return new RewriteReflectiveFacade.JavaParserBuilder(getClassLoader() + .loadClass("org.openrewrite.java.Java11Parser") + .getMethod("builder") + .invoke(null)); + } catch (Exception e) { + throw new RuntimeException(e); + } + } +} diff --git a/build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/rewrite/RewriteTask.java b/build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/rewrite/RewriteTask.java new file mode 100644 index 000000000000..cfaed17609f2 --- /dev/null +++ b/build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/rewrite/RewriteTask.java @@ -0,0 +1,114 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +package org.elasticsearch.gradle.internal.rewrite; + +import org.gradle.api.DefaultTask; +import org.gradle.api.artifacts.Configuration; +import org.gradle.api.file.ConfigurableFileCollection; +import org.gradle.api.file.RegularFileProperty; +import org.gradle.api.provider.Property; +import org.gradle.api.provider.SetProperty; +import org.gradle.api.tasks.Input; +import org.gradle.api.tasks.InputFile; +import org.gradle.api.tasks.InputFiles; +import org.gradle.api.tasks.Internal; +import org.gradle.api.tasks.TaskAction; +import org.gradle.workers.WorkQueue; +import org.gradle.workers.WorkerExecutor; + +import javax.inject.Inject; +import java.io.File; +import java.util.List; +import java.util.Set; + +import static java.util.stream.Collectors.toList; + +public abstract class RewriteTask extends DefaultTask { + + private final Configuration configuration; + private WorkerExecutor workerExecutor; + protected final RewriteExtension extension; + + @InputFiles + abstract ConfigurableFileCollection getSourceFiles(); + + @InputFiles + abstract ConfigurableFileCollection getDependencyFiles(); + + @Input + abstract SetProperty getActiveRecipes(); + + @InputFile + abstract RegularFileProperty getConfigFile(); + + @Internal + abstract Property getMaxHeapSize(); + + @Inject + public RewriteTask( + Configuration configuration, + RewriteExtension extension, + WorkerExecutor workerExecutor + ) { + this.configuration = configuration; + this.extension = extension; + this.workerExecutor = workerExecutor; + this.setGroup("rewrite"); + this.setDescription("Apply the active refactoring recipes"); + } + + @TaskAction + public void run() { + WorkQueue workQueue = workerExecutor.processIsolation(spec -> { + spec.getClasspath().from(configuration); + spec.getForkOptions().jvmArgs("--add-exports"); + spec.getForkOptions().jvmArgs("jdk.compiler/com.sun.tools.javac.code=ALL-UNNAMED"); + + spec.getForkOptions().jvmArgs("--add-exports"); + spec.getForkOptions().jvmArgs("jdk.compiler/com.sun.tools.javac.main=ALL-UNNAMED"); + + spec.getForkOptions().jvmArgs("--add-exports"); + spec.getForkOptions().jvmArgs("jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED"); + + spec.getForkOptions().jvmArgs("--add-exports"); + spec.getForkOptions().jvmArgs("jdk.compiler/com.sun.tools.javac.comp=ALL-UNNAMED"); + + spec.getForkOptions().jvmArgs("--add-exports"); + spec.getForkOptions().jvmArgs("jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED"); + + spec.getForkOptions().jvmArgs("--add-exports"); + spec.getForkOptions().jvmArgs("jdk.compiler/com.sun.tools.javac.parser=ALL-UNNAMED"); + + spec.getForkOptions().jvmArgs("--add-exports"); + spec.getForkOptions().jvmArgs("jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED"); + + spec.getForkOptions().jvmArgs("--add-exports"); + spec.getForkOptions().jvmArgs("jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED"); + spec.getForkOptions().workingDir(getProject().getProjectDir()); + spec.getForkOptions().setMaxHeapSize(getMaxHeapSize().convention("1g").get()); + }); + + List javaPaths = getSourceFiles().getFiles() + .stream() + .filter(it -> it.isFile() && it.getName().endsWith(".java")) + .collect(toList()); + + if (javaPaths.size() > 0) { + Set dependencyPaths = getDependencyFiles().getFiles(); + + workQueue.submit(RewriteWorker.class, parameters -> { + parameters.getAllJavaPaths().addAll(javaPaths); + parameters.getAllDependencyPaths().addAll(dependencyPaths); + parameters.getActiveRecipes().addAll(getActiveRecipes().get()); + parameters.getProjectDirectory().fileProvider(getProject().getProviders().provider(() -> getProject().getProjectDir())); + parameters.getConfigFile().value(getConfigFile()); + }); + } + } +} diff --git a/build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/rewrite/RewriteWorker.java b/build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/rewrite/RewriteWorker.java new file mode 100644 index 000000000000..b9712bc8ea99 --- /dev/null +++ b/build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/rewrite/RewriteWorker.java @@ -0,0 +1,194 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +package org.elasticsearch.gradle.internal.rewrite; + +import org.gradle.api.logging.Logger; +import org.gradle.api.logging.Logging; +import org.gradle.workers.WorkAction; + +import java.io.BufferedWriter; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.nio.charset.Charset; +import java.nio.file.Files; +import java.nio.file.Path; +import java.time.Instant; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +import java.util.Properties; + +import static java.util.Collections.emptyList; +import static java.util.stream.Collectors.toList; + +public abstract class RewriteWorker implements WorkAction { + + private static Logger logger = Logging.getLogger(RewriteWorker.class); + + RewriteReflectiveFacade rewrite; + @Override + public void execute() { + rewrite = new RewriteReflectiveFacade(); + ResultsContainer results = listResults(); + writeInPlaceChanges(results); + } + + private void writeInPlaceChanges(ResultsContainer results) { + for (RewriteReflectiveFacade.Result result : results.refactoredInPlace) { + assert result.getBefore() != null; + try (BufferedWriter sourceFileWriter = Files.newBufferedWriter( + results.getProjectRoot().resolve(result.getBefore().getSourcePath()))) { + assert result.getAfter() != null; + sourceFileWriter.write(result.getAfter().print()); + } catch (IOException e) { + e.printStackTrace(); + } + } + } + + protected RewriteWorker.ResultsContainer listResults() { + Path baseDir = getParameters().getProjectDirectory().get().getAsFile().toPath(); + RewriteReflectiveFacade.Environment env = environment(); + List activeRecipes = getParameters().getActiveRecipes().get(); + List activeStyles = getParameters().getActiveStyles().get(); + logger.lifecycle(String.format("Using active recipe(s) %s", activeRecipes)); + logger.lifecycle(String.format("Using active styles(s) %s", activeStyles)); + if (activeRecipes.isEmpty()) { + return new RewriteWorker.ResultsContainer(baseDir, emptyList()); + } + List styles = env.activateStyles(activeStyles); + RewriteReflectiveFacade.Recipe recipe = env.activateRecipes(activeRecipes); + logger.lifecycle("Validating active recipes"); + Collection validated = recipe.validateAll(); + List failedValidations = validated.stream() + .map(RewriteReflectiveFacade.Validated::failures) + .flatMap(Collection::stream) + .collect(toList()); + if (failedValidations.isEmpty() == false) { + failedValidations.forEach( + failedValidation -> logger.error( + "Recipe validation error in " + failedValidation.getProperty() + ": " + failedValidation.getMessage(), + failedValidation.getException() + ) + ); + logger.error( + "Recipe validation errors detected as part of one or more activeRecipe(s). Execution will continue regardless." + ); + } + + RewriteReflectiveFacade.InMemoryExecutionContext ctx = executionContext(); + List sourceFiles = parse( + getParameters().getAllJavaPaths().get(), + getParameters().getAllDependencyPaths().get(), + styles, + ctx); + + logger.lifecycle("Running recipe(s)..."); + List results = recipe.run(sourceFiles); + return new RewriteWorker.ResultsContainer(baseDir, results); + } + + protected RewriteReflectiveFacade.InMemoryExecutionContext executionContext() { + return rewrite.inMemoryExecutionContext(t -> logger.warn(t.getMessage(), t)); + } + + protected List parse( + List javaFiles, + List dependencyFiles, + List styles, + RewriteReflectiveFacade.InMemoryExecutionContext ctx + ) { + try { + List javaPaths = javaFiles.stream().map(File::toPath).map(p -> p.toAbsolutePath()).toList(); + List dependencyPaths = dependencyFiles.stream().map(File::toPath).map(p -> p.toAbsolutePath()).toList(); + List sourceFiles = new ArrayList<>(); + if (javaPaths.size() > 0) { + logger.lifecycle( + "Parsing " + javaPaths.size() + " Java files from " + getParameters().getProjectDirectory().getAsFile().get() + ); + Instant start = Instant.now(); + sourceFiles.addAll( + rewrite.javaParserFromJavaVersion() + .relaxedClassTypeMatching(true) + .styles(styles) + .classpath(dependencyPaths) + .charset(Charset.forName("UTF-8")) + .logCompilationWarningsAndErrors(false) + .build() + .parse(javaPaths, getParameters().getProjectDirectory().get().getAsFile().toPath(), ctx) + ); + } + return sourceFiles; + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + protected RewriteReflectiveFacade.Environment environment() { + Properties properties = new Properties(); + RewriteReflectiveFacade.EnvironmentBuilder env = + rewrite.environmentBuilder(properties).scanRuntimeClasspath().scanUserHome(); + File rewriteConfig = getParameters().getConfigFile().getAsFile().getOrNull(); + if(rewriteConfig != null){ + if (rewriteConfig.exists()) { + try (FileInputStream is = new FileInputStream(rewriteConfig)) { + RewriteReflectiveFacade.YamlResourceLoader resourceLoader = + rewrite.yamlResourceLoader(is, rewriteConfig.toURI(), properties); + env.load(resourceLoader); + } catch (IOException e) { + throw new RuntimeException("Unable to load rewrite configuration", e); + } + } else { + logger.warn("Rewrite configuration file " + rewriteConfig + " does not exist."); + } + } + return env.build(); + } + + public static class ResultsContainer { + final Path projectRoot; + final List generated = new ArrayList<>(); + final List deleted = new ArrayList<>(); + final List moved = new ArrayList<>(); + final List refactoredInPlace = new ArrayList<>(); + + public ResultsContainer(Path projectRoot, Collection results) { + this.projectRoot = projectRoot; + for (RewriteReflectiveFacade.Result result : results) { + if (result.getBefore() == null && result.getAfter() == null) { + // This situation shouldn't happen / makes no sense, log and skip + continue; + } + if (result.getBefore() == null && result.getAfter() != null) { + generated.add(result); + } else if (result.getBefore() != null && result.getAfter() == null) { + deleted.add(result); + } else if (result.getBefore() != null + && result.getBefore().getSourcePath().equals(result.getAfter().getSourcePath()) == false) { + moved.add(result); + } else { + refactoredInPlace.add(result); + } + } + } + + public Path getProjectRoot() { + return projectRoot; + } + + public boolean isNotEmpty() { + return generated.isEmpty() == false + || deleted.isEmpty() == false + || moved.isEmpty() == false + || refactoredInPlace.isEmpty() == false; + } + } +} diff --git a/build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/rewrite/rules/ChangeMethodOwnerRecipe.java b/build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/rewrite/rules/ChangeMethodOwnerRecipe.java new file mode 100644 index 000000000000..fc799e79cfb9 --- /dev/null +++ b/build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/rewrite/rules/ChangeMethodOwnerRecipe.java @@ -0,0 +1,31 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +package org.elasticsearch.gradle.internal.rewrite.rules; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import org.openrewrite.Recipe; +import org.openrewrite.internal.lang.NonNull; + +public class ChangeMethodOwnerRecipe extends Recipe { + @Override + public String getDisplayName() { + return "Change Method Owner Recipe chain"; + } + + @JsonCreator + public ChangeMethodOwnerRecipe(@NonNull @JsonProperty("originFullQualifiedClassname") String originFullQualifiedClassname, + @NonNull @JsonProperty("methodName") String methodName, + @NonNull @JsonProperty("targetFullQualifiedClassname") String targetFullQualifiedClassname) { + doNext(new FullQualifiedChangeMethodOwnerRecipe(originFullQualifiedClassname, methodName, targetFullQualifiedClassname)); + doNext(new FixFullQualifiedReferenceRecipe(targetFullQualifiedClassname, true)); + } + +} + diff --git a/build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/rewrite/rules/FixFullQualifiedReferenceRecipe.java b/build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/rewrite/rules/FixFullQualifiedReferenceRecipe.java new file mode 100644 index 000000000000..74e02c15b9d5 --- /dev/null +++ b/build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/rewrite/rules/FixFullQualifiedReferenceRecipe.java @@ -0,0 +1,121 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +package org.elasticsearch.gradle.internal.rewrite.rules; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import org.openrewrite.ExecutionContext; +import org.openrewrite.Option; +import org.openrewrite.Recipe; +import org.openrewrite.internal.lang.NonNull; +import org.openrewrite.java.JavaIsoVisitor; +import org.openrewrite.java.JavaVisitor; +import org.openrewrite.java.tree.Expression; +import org.openrewrite.java.tree.J; +import org.openrewrite.java.tree.JavaType; + +import java.util.Collections; +import java.util.Set; + +import static org.elasticsearch.gradle.internal.rewrite.rules.FullQualifiedChangeMethodOwnerRecipe.METHOD_CHANGE_PREFIX; + +public class FixFullQualifiedReferenceRecipe extends Recipe { + + @Option( + displayName = "Fully-qualified target type name", + description = "A fully-qualified class name we want to fix.", + example = "org.elasticsearch.core.List" + ) + private String fullQualifiedClassname; + + @Option( + displayName = "Only on change flag", + description = "A flag indicating if this rule should only be applied on changed methods.", + example = "true" + ) + private boolean onlyOnChangedSource; + + @JsonCreator + public FixFullQualifiedReferenceRecipe( + @NonNull @JsonProperty("fullQualifiedClassname") String fullQualifiedClassname, + @NonNull @JsonProperty("onlyOnChangedSource") boolean onlyOnChangedSource + + ) { + this.fullQualifiedClassname = fullQualifiedClassname; + this.onlyOnChangedSource = onlyOnChangedSource; + } + + @Override + public String getDisplayName() { + return "FixFullQualifiedReferenceRecipe"; + } + + @Override + public String getDescription() { + return "Converts full qualified method calls to simple calls and an import if no clashing imports are found."; + } + + @Override + protected JavaVisitor getVisitor() { + String unqualifiedIdentifier = fullQualifiedClassname.substring(fullQualifiedClassname.lastIndexOf('.') + 1); + return new Visitor(fullQualifiedClassname, unqualifiedIdentifier, onlyOnChangedSource); + } + + public static class Visitor extends JavaIsoVisitor { + + private String fullQualifiedClassname; + private String unqualifiedIdentifier; + private boolean hasOriginImport; + private boolean onlyOnChangedSource; + + public Visitor(String fullQualifiedClassname, String unqualifiedIdentifier, boolean onlyOnChangedSource) { + this.fullQualifiedClassname = fullQualifiedClassname; + this.unqualifiedIdentifier = unqualifiedIdentifier; + this.onlyOnChangedSource = onlyOnChangedSource; + } + + @Override + public J.MethodInvocation visitMethodInvocation(J.MethodInvocation method, ExecutionContext executionContext) { + J.MethodInvocation m = super.visitMethodInvocation(method, executionContext); + if (canChange(method, executionContext) + && hasOriginImport == false + && m.getSelect() instanceof J.FieldAccess + && ((J.FieldAccess) m.getSelect()).isFullyQualifiedClassReference(fullQualifiedClassname)) { + Expression select = m.getSelect(); + JavaType javaType = JavaType.buildType(fullQualifiedClassname); + J.Identifier list = J.Identifier.build( + select.getId(), + select.getPrefix(), + select.getMarkers(), + unqualifiedIdentifier, + javaType + ); + m = m.withSelect(list); + maybeAddImport(fullQualifiedClassname); + } + return m; + } + + private boolean canChange(J.MethodInvocation method, ExecutionContext executionContext) { + if (onlyOnChangedSource == false) { + return true; + } + + Set processed = executionContext.getMessage(METHOD_CHANGE_PREFIX + fullQualifiedClassname, Collections.emptySet()); + return processed.contains(method.getId()); + } + + @Override + public J.CompilationUnit visitCompilationUnit(J.CompilationUnit cu, ExecutionContext executionContext) { + hasOriginImport = cu.getImports().stream().anyMatch(i -> i.getClassName().equals(unqualifiedIdentifier)); + return super.visitCompilationUnit(cu, executionContext); + } + } + +} diff --git a/build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/rewrite/rules/FullQualifiedChangeMethodOwnerRecipe.java b/build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/rewrite/rules/FullQualifiedChangeMethodOwnerRecipe.java new file mode 100644 index 000000000000..6e4f5d5666b8 --- /dev/null +++ b/build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/rewrite/rules/FullQualifiedChangeMethodOwnerRecipe.java @@ -0,0 +1,140 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +package org.elasticsearch.gradle.internal.rewrite.rules; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import org.openrewrite.ExecutionContext; +import org.openrewrite.Option; +import org.openrewrite.Recipe; +import org.openrewrite.internal.lang.NonNull; +import org.openrewrite.java.JavaIsoVisitor; +import org.openrewrite.java.JavaTemplate; +import org.openrewrite.java.JavaVisitor; +import org.openrewrite.java.MethodMatcher; +import org.openrewrite.java.tree.J; +import org.openrewrite.java.tree.JavaType; + +import java.util.Objects; +import java.util.UUID; + +import static java.util.stream.Collectors.joining; +import static java.util.stream.IntStream.rangeClosed; + +public class FullQualifiedChangeMethodOwnerRecipe extends Recipe { + + public static final String METHOD_CHANGE_PREFIX = FullQualifiedChangeMethodOwnerRecipe.class.getSimpleName() + "-CHANGED"; + @Option( + displayName = "Fully-qualified origin class name", + description = "A fully-qualified class name of the type upon which the method is defined.", + example = "java.util.List" + ) + private String originFullQualifiedClassname; + + @Option( + displayName = "method name", + description = "The name of the method we want to change the origin.", + example = "of" + ) + private String originMethod; + + @Option( + displayName = "Fully-qualified target type name", + description = "A fully-qualified class name of the type we want to change the method call to.", + example = "org.elasticsearch.core.List" + ) + private String targetFullQualifiedClassname; + + @JsonCreator + public FullQualifiedChangeMethodOwnerRecipe( + @NonNull @JsonProperty("originFullQualifiedClassname") String originFullQualifiedClassname, + @NonNull @JsonProperty("originMethod") String originMethod, + @NonNull @JsonProperty("targetFullQualifiedClassname") String targetFullQualifiedClassname + ) { + this.originFullQualifiedClassname = originFullQualifiedClassname; + this.originMethod = originMethod; + this.targetFullQualifiedClassname = targetFullQualifiedClassname; + } + + @Override + public String getDisplayName() { + return "FullQualifiedListOfBackportRecipe"; + } + + @Override + public String getDescription() { + return "Converts owner of a method call and uses full qualified type to avoid import conflicts."; + } + + @Override + protected JavaVisitor getVisitor() { + return new Visitor(originFullQualifiedClassname, originMethod, targetFullQualifiedClassname); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + if (super.equals(o) == false) return false; + FullQualifiedChangeMethodOwnerRecipe that = (FullQualifiedChangeMethodOwnerRecipe) o; + return Objects.equals(originFullQualifiedClassname, that.originFullQualifiedClassname) + && Objects.equals(originMethod, that.originMethod) + && Objects.equals(targetFullQualifiedClassname, that.targetFullQualifiedClassname); + } + + @Override + public int hashCode() { + return Objects.hash(super.hashCode(), originFullQualifiedClassname, originMethod, targetFullQualifiedClassname); + } + + public static class Visitor extends JavaIsoVisitor { + private final MethodMatcher methodMatcher; + private String originFullQualifiedClassname; + private String originMethod; + private String targetFullQualifiedClassname; + + public Visitor(MethodMatcher methodMatcher) { + this.methodMatcher = methodMatcher; + } + + public Visitor(String originFullQualifiedClassname, + String originMethod, + String targetFullQualifiedClassname) { + this.originFullQualifiedClassname = originFullQualifiedClassname; + this.originMethod = originMethod; + this.targetFullQualifiedClassname = targetFullQualifiedClassname; + methodMatcher = new MethodMatcher(originFullQualifiedClassname + " " + originMethod + "(..)"); + } + + @Override + public J.MethodInvocation visitMethodInvocation(J.MethodInvocation method, ExecutionContext executionContext) { + J.MethodInvocation mi = super.visitMethodInvocation(method, executionContext); + JavaType.Method type = method.getType(); + if (type != null && methodMatcher.matches(method)) { + int paramCount = method.getArguments().size(); + String code = targetFullQualifiedClassname + + "." + + originMethod + + "(" + + rangeClosed(1, paramCount).mapToObj(i -> "#{any()}").collect(joining(", ")) + + ")"; + JavaTemplate listOfUsage = JavaTemplate.builder(this::getCursor, code).build(); + mi = method.withTemplate(listOfUsage, method.getCoordinates().replace(), method.getArguments().toArray()); + maybeRemoveImport(originFullQualifiedClassname); + trackChange(executionContext, mi.getId()); + } + return mi; + } + + private void trackChange(ExecutionContext executionContext, UUID id) { + executionContext.putMessageInSet(METHOD_CHANGE_PREFIX + targetFullQualifiedClassname, id); + } + + } +} diff --git a/build.gradle b/build.gradle index 1a9828dfbd0c..12c49b89f8e4 100644 --- a/build.gradle +++ b/build.gradle @@ -13,6 +13,7 @@ import org.elasticsearch.gradle.internal.BuildPlugin import org.elasticsearch.gradle.Version import org.elasticsearch.gradle.VersionProperties import org.elasticsearch.gradle.internal.info.BuildParams +import org.elasticsearch.gradle.internal.rewrite.RewritePlugin import org.elasticsearch.gradle.plugin.PluginBuildPlugin import org.gradle.plugins.ide.eclipse.model.AccessRule import org.gradle.util.DistributionLocator @@ -43,6 +44,7 @@ plugins { id 'elasticsearch.internal-testclusters' id 'elasticsearch.run' id 'elasticsearch.release-tools' + id 'elasticsearch.auto-backporting' id "com.diffplug.spotless" version "5.15.1" apply false } diff --git a/rewrite.yml b/rewrite.yml new file mode 100644 index 000000000000..603826b7936d --- /dev/null +++ b/rewrite.yml @@ -0,0 +1,35 @@ +type: specs.openrewrite.org/v1beta/recipe +name: org.elasticsearch.java.backport.ListOfBackport +displayName: Use `org.elasticsearch.core.Lists#of(..)` not java.util.List.of#(..) +description: Java 8 does not support the `java.util.List#of(..)`. +tags: + - backport +recipeList: + - org.elasticsearch.gradle.internal.rewrite.rules.ChangeMethodOwnerRecipe: + originFullQualifiedClassname: java.util.List + targetFullQualifiedClassname: org.elasticsearch.core.List + methodName: of +--- +type: specs.openrewrite.org/v1beta/recipe +name: org.elasticsearch.java.backport.MapOfBackport +displayName: Use `org.elasticsearch.core.Maps#of(..)` not java.util.Map.of#(..) +description: Java 8 does not support the `java.util.Map#of(..)`. +tags: + - backport +recipeList: + - org.elasticsearch.gradle.internal.rewrite.rules.ChangeMethodOwnerRecipe: + originFullQualifiedClassname: java.util.Map + targetFullQualifiedClassname: org.elasticsearch.core.Map + methodName: of +--- +type: specs.openrewrite.org/v1beta/recipe +name: org.elasticsearch.java.backport.SetOfBackport +displayName: Use `org.elasticsearch.core.Sets#of(..)` not java.util.Set.of#(..) +description: Java 8 does not support the `java.util.Set#of(..)`. +tags: + - backport +recipeList: + - org.elasticsearch.gradle.internal.rewrite.rules.ChangeMethodOwnerRecipe: + originFullQualifiedClassname: java.util.Set + targetFullQualifiedClassname: org.elasticsearch.core.Set + methodName: of \ No newline at end of file