diff --git a/pom.xml b/pom.xml index 03e1f42..f3af8db 100644 --- a/pom.xml +++ b/pom.xml @@ -15,6 +15,7 @@ spring-rewrite-commons-launcher spring-rewrite-commons-docs spring-rewrite-commons-examples + spring-rewrite-commons-gradle @@ -30,6 +31,8 @@ 3.1.3 8.5.1 + 1.8.2 + 1.8.9 5.3.2 2.3.1 diff --git a/spring-rewrite-commons-gradle/README.md b/spring-rewrite-commons-gradle/README.md new file mode 100644 index 0000000..03675cd --- /dev/null +++ b/spring-rewrite-commons-gradle/README.md @@ -0,0 +1,16 @@ +# Gradle Project Parser + +## Structure +**Model** Shared data structures between the **plugin** and **parser** modules. It has a utility class `SpringRewriteModelBuilder` to fetch `GradleProjectData` from Gradel Build process + +**Plugin** Gradle plugin that executes the code inside of Gradle Build process. Responsible for creating serializable instance of `SpringRewriteModelBuilder` + +**Parser** Parses Gradle project sources with a help of `GradleProjectData` fetched from Gradle Build process via Gradle Tooling API + +## Building +Execute first `mvn clean install -DskipTests` to install **plugin** based on the latest sources into local Maven repository + +Then execute `mvn clean install` - now tests would be able to find the **plugin** module for their execution. + +## Usage +See `ProjectParserTest` class under the tests of **parser** module diff --git a/spring-rewrite-commons-gradle/pom.xml b/spring-rewrite-commons-gradle/pom.xml new file mode 100644 index 0000000..426c903 --- /dev/null +++ b/spring-rewrite-commons-gradle/pom.xml @@ -0,0 +1,35 @@ + + + 4.0.0 + + org.springframework.rewrite + spring-rewrite-commons + 0.1.0-SNAPSHOT + + + spring-rewrite-commons-gradle + pom + + + 17 + 17 + UTF-8 + 8.4 + + + + rewrite-gradle-model + rewrite-gradle-plugin + rewrite-gradle-parser + + + + + gradle + https://repo.gradle.org/gradle/libs-releases + + + + \ No newline at end of file diff --git a/spring-rewrite-commons-gradle/rewrite-gradle-model/pom.xml b/spring-rewrite-commons-gradle/rewrite-gradle-model/pom.xml new file mode 100644 index 0000000..13d912a --- /dev/null +++ b/spring-rewrite-commons-gradle/rewrite-gradle-model/pom.xml @@ -0,0 +1,48 @@ + + + 4.0.0 + + org.springframework.rewrite + spring-rewrite-commons-gradle + 0.1.0-SNAPSHOT + + + rewrite-gradle-model + + + 17 + 17 + UTF-8 + + + + + org.openrewrite.gradle.tooling + model + 1.1.2 + + + org.gradle + gradle-tooling-api + ${gradle.version} + + + org.junit.jupiter + junit-jupiter-engine + test + + + org.junit.jupiter + junit-jupiter-api + test + + + org.assertj + assertj-core + test + + + + \ No newline at end of file diff --git a/spring-rewrite-commons-gradle/rewrite-gradle-model/src/main/java/org/springframework/rewrite/gradle/model/GradleProjectData.java b/spring-rewrite-commons-gradle/rewrite-gradle-model/src/main/java/org/springframework/rewrite/gradle/model/GradleProjectData.java new file mode 100644 index 0000000..8cabc9f --- /dev/null +++ b/spring-rewrite-commons-gradle/rewrite-gradle-model/src/main/java/org/springframework/rewrite/gradle/model/GradleProjectData.java @@ -0,0 +1,60 @@ +/* + * Copyright 2021 - 2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.rewrite.gradle.model; + +import org.openrewrite.gradle.toolingapi.GradleProject; +import org.openrewrite.gradle.toolingapi.GradleSettings; + +import java.io.File; +import java.util.Collection; +import java.util.List; +import java.util.Map; + +public interface GradleProjectData extends GradleProject { + + String getGroup(); + + String getVersion(); + + GradleSettings getGradleSettings(); + + String getGradleVersion(); + + boolean isRootProject(); + + File getRootProjectDir(); + + Collection getSubprojects(); + + File getProjectDir(); + + File getBuildDir(); + + File getBuildscriptFile(); + + Map getProperties(); + + List getJavaSourceSets(); + + boolean isMultiPlatformKotlinProject(); + + List getKotlinSourceSets(); + + Collection getBuildscriptClasspath(); + + Collection getSettingsClasspath(); + +} diff --git a/spring-rewrite-commons-gradle/rewrite-gradle-model/src/main/java/org/springframework/rewrite/gradle/model/JavaSourceSetData.java b/spring-rewrite-commons-gradle/rewrite-gradle-model/src/main/java/org/springframework/rewrite/gradle/model/JavaSourceSetData.java new file mode 100644 index 0000000..eba4c44 --- /dev/null +++ b/spring-rewrite-commons-gradle/rewrite-gradle-model/src/main/java/org/springframework/rewrite/gradle/model/JavaSourceSetData.java @@ -0,0 +1,39 @@ +/* + * Copyright 2021 - 2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.rewrite.gradle.model; + +import java.io.File; +import java.util.Collection; + +public interface JavaSourceSetData { + + String getName(); + + Collection getSources(); + + Collection getSourceDirectories(); + + Collection getJava(); + + Collection getClassesDirs(); + + Collection getCompileClasspath(); + + Collection getImplementationClasspath(); + + JavaVersionData getJavaVersionData(); + +} diff --git a/spring-rewrite-commons-gradle/rewrite-gradle-model/src/main/java/org/springframework/rewrite/gradle/model/JavaVersionData.java b/spring-rewrite-commons-gradle/rewrite-gradle-model/src/main/java/org/springframework/rewrite/gradle/model/JavaVersionData.java new file mode 100644 index 0000000..a31c729 --- /dev/null +++ b/spring-rewrite-commons-gradle/rewrite-gradle-model/src/main/java/org/springframework/rewrite/gradle/model/JavaVersionData.java @@ -0,0 +1,28 @@ +/* + * Copyright 2021 - 2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.rewrite.gradle.model; + +public interface JavaVersionData { + + String getCreatedBy(); + + String getVmVendor(); + + String getSourceCompatibility(); + + String getTargetCompatibility(); + +} diff --git a/spring-rewrite-commons-gradle/rewrite-gradle-model/src/main/java/org/springframework/rewrite/gradle/model/KotlinSourceSetData.java b/spring-rewrite-commons-gradle/rewrite-gradle-model/src/main/java/org/springframework/rewrite/gradle/model/KotlinSourceSetData.java new file mode 100644 index 0000000..ebe4117 --- /dev/null +++ b/spring-rewrite-commons-gradle/rewrite-gradle-model/src/main/java/org/springframework/rewrite/gradle/model/KotlinSourceSetData.java @@ -0,0 +1,31 @@ +/* + * Copyright 2021 - 2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.rewrite.gradle.model; + +import java.io.File; +import java.util.Collection; + +public interface KotlinSourceSetData { + + String getName(); + + Collection getKotlin(); + + Collection getCompileClasspath(); + + Collection getImplementationClasspath(); + +} diff --git a/spring-rewrite-commons-gradle/rewrite-gradle-model/src/main/java/org/springframework/rewrite/gradle/model/SpringRewriteModelBuilder.java b/spring-rewrite-commons-gradle/rewrite-gradle-model/src/main/java/org/springframework/rewrite/gradle/model/SpringRewriteModelBuilder.java new file mode 100644 index 0000000..969e8d4 --- /dev/null +++ b/spring-rewrite-commons-gradle/rewrite-gradle-model/src/main/java/org/springframework/rewrite/gradle/model/SpringRewriteModelBuilder.java @@ -0,0 +1,81 @@ +/* + * Copyright 2021 - 2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.rewrite.gradle.model; + +import org.gradle.tooling.GradleConnector; +import org.gradle.tooling.ModelBuilder; +import org.gradle.tooling.ProjectConnection; +import org.gradle.tooling.internal.consumer.DefaultGradleConnector; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.UncheckedIOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardCopyOption; +import java.util.ArrayList; +import java.util.List; + +public class SpringRewriteModelBuilder { + + public static T forProjectDirectory(Class type, File projectDir, File buildFile) { + DefaultGradleConnector connector = (DefaultGradleConnector) GradleConnector.newConnector(); + if (Files.exists(projectDir.toPath().resolve("gradle/wrapper/gradle-wrapper.properties"))) { + connector.useBuildDistribution(); + } + else { + connector.useGradleVersion("8.4"); + } + connector.forProjectDirectory(projectDir); + List arguments = new ArrayList<>(); + if (buildFile != null && buildFile.exists()) { + arguments.add("-b"); + arguments.add(buildFile.getAbsolutePath()); + } + arguments.add("--init-script"); + Path init = projectDir.toPath().resolve("openrewrite-tooling.gradle").toAbsolutePath(); + arguments.add(init.toString()); + try (ProjectConnection connection = connector.connect()) { + ModelBuilder customModelBuilder = connection.model(type); + try (InputStream is = SpringRewriteModelBuilder.class.getResourceAsStream("/init.gradle")) { + if (is == null) { + throw new IllegalStateException("Expected to find init.gradle on the classpath"); + } + Files.copy(is, init, StandardCopyOption.REPLACE_EXISTING); + customModelBuilder.withArguments(arguments); + return customModelBuilder.get(); + } + catch (IOException e) { + throw new UncheckedIOException(e); + } + finally { + try { + Files.delete(init); + } + catch (IOException e) { + // noinspection ThrowFromFinallyBlock + throw new UncheckedIOException(e); + } + } + } + } + + public static GradleProjectData forProjectDirectory(File projectDir, File buildFile) { + return forProjectDirectory(GradleProjectData.class, projectDir, buildFile); + } + +} diff --git a/spring-rewrite-commons-gradle/rewrite-gradle-model/src/main/resources/init.gradle b/spring-rewrite-commons-gradle/rewrite-gradle-model/src/main/resources/init.gradle new file mode 100644 index 0000000..9d0f67d --- /dev/null +++ b/spring-rewrite-commons-gradle/rewrite-gradle-model/src/main/resources/init.gradle @@ -0,0 +1,37 @@ +/* + * Copyright 2021 - 2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +initscript{ + repositories{ + mavenLocal() + mavenCentral() + } + + configurations.all{ + resolutionStrategy{ + cacheChangingModulesFor 0, 'seconds' + cacheDynamicVersionsFor 0, 'seconds' + } + } + + dependencies{ + classpath 'org.springframework.rewrite:rewrite-gradle-plugin:latest.integration' + classpath 'org.springframework.rewrite:rewrite-gradle-model:latest.integration' + } +} + +allprojects{ + apply plugin: org.springframework.rewrite.gradle.plugin.SpringRewritePlugin +} diff --git a/spring-rewrite-commons-gradle/rewrite-gradle-model/src/test/java/org/springframework/rewrite/gradle/model/GradleProjectDataTest.java b/spring-rewrite-commons-gradle/rewrite-gradle-model/src/test/java/org/springframework/rewrite/gradle/model/GradleProjectDataTest.java new file mode 100644 index 0000000..5825c9f --- /dev/null +++ b/spring-rewrite-commons-gradle/rewrite-gradle-model/src/test/java/org/springframework/rewrite/gradle/model/GradleProjectDataTest.java @@ -0,0 +1,49 @@ +/* + * Copyright 2021 - 2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.rewrite.gradle.model; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +import java.io.InputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Objects; + +import static org.assertj.core.api.Assertions.assertThat; + +public class GradleProjectDataTest { + + @Test + void serializable(@TempDir Path dir) throws Exception { + try (InputStream is = GradleProjectDataTest.class.getResourceAsStream("/build.gradle")) { + Files.write(dir.resolve("build.gradle"), Objects.requireNonNull(is).readAllBytes()); + } + try (InputStream is = GradleProjectDataTest.class.getResourceAsStream("/settings.gradle")) { + Files.write(dir.resolve("settings.gradle"), Objects.requireNonNull(is).readAllBytes()); + } + + GradleProjectData gp = SpringRewriteModelBuilder.forProjectDirectory(GradleProjectData.class, dir.toFile(), + dir.resolve("build.gradle").toFile()); + + assertThat(gp.getGroup()).isEqualTo(""); + assertThat(gp.getName()).isEqualTo("sample"); + assertThat(gp.getVersion()).isEqualTo("unspecified"); + assertThat(gp.getPlugins().size()).isEqualTo(13); + assertThat(gp.getMavenRepositories().get(0).getUri()).isEqualTo("https://repo.maven.apache.org/maven2/"); + } + +} diff --git a/spring-rewrite-commons-gradle/rewrite-gradle-model/src/test/resources/build.gradle b/spring-rewrite-commons-gradle/rewrite-gradle-model/src/test/resources/build.gradle new file mode 100644 index 0000000..c0425eb --- /dev/null +++ b/spring-rewrite-commons-gradle/rewrite-gradle-model/src/test/resources/build.gradle @@ -0,0 +1,27 @@ +/* + * Copyright 2021 - 2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +plugins{ + id 'java' +} + +repositories{ + mavenCentral() +} + +dependencies{ + implementation 'org.apache.commons:commons-lang3:3.4' + testImplementation("org.projectlombok:lombok:latest.release") +} diff --git a/spring-rewrite-commons-gradle/rewrite-gradle-model/src/test/resources/settings.gradle b/spring-rewrite-commons-gradle/rewrite-gradle-model/src/test/resources/settings.gradle new file mode 100644 index 0000000..2eb6952 --- /dev/null +++ b/spring-rewrite-commons-gradle/rewrite-gradle-model/src/test/resources/settings.gradle @@ -0,0 +1,16 @@ +/* + * Copyright 2021 - 2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +rootProject.name = "sample" diff --git a/spring-rewrite-commons-gradle/rewrite-gradle-parser/pom.xml b/spring-rewrite-commons-gradle/rewrite-gradle-parser/pom.xml new file mode 100644 index 0000000..936cd34 --- /dev/null +++ b/spring-rewrite-commons-gradle/rewrite-gradle-parser/pom.xml @@ -0,0 +1,124 @@ + + + 4.0.0 + + org.springframework.rewrite + spring-rewrite-commons-gradle + 0.1.0-SNAPSHOT + + + rewrite-gradle-parser + + + 17 + 17 + UTF-8 + + + + + org.springframework.rewrite + rewrite-gradle-model + 0.1.0-SNAPSHOT + + + + dev.gradleplugins + gradle-api + ${gradle.version} + + + org.openrewrite + rewrite-core + ${rewrite.version} + + + org.openrewrite + rewrite-gradle + ${rewrite.version} + + + org.openrewrite + rewrite-groovy + ${rewrite.version} + + + org.openrewrite + rewrite-hcl + ${rewrite.version} + + + org.openrewrite + rewrite-java + ${rewrite.version} + + + org.openrewrite + rewrite-java-8 + ${rewrite.version} + + + org.openrewrite + rewrite-java-11 + ${rewrite.version} + + + org.openrewrite + rewrite-java-17 + ${rewrite.version} + + + org.openrewrite + rewrite-json + ${rewrite.version} + + + org.openrewrite + rewrite-kotlin + ${rewrite-kotlin.version} + + + org.openrewrite + rewrite-properties + ${rewrite.version} + + + org.openrewrite + rewrite-protobuf + ${rewrite.version} + + + org.openrewrite + rewrite-xml + ${rewrite.version} + + + org.openrewrite + rewrite-yaml + ${rewrite.version} + + + org.openrewrite + rewrite-polyglot + ${rewrite-polyglot.version} + + + org.junit.jupiter + junit-jupiter-engine + test + + + org.junit.jupiter + junit-jupiter-api + test + + + org.assertj + assertj-core + test + + + + \ No newline at end of file diff --git a/spring-rewrite-commons-gradle/rewrite-gradle-parser/src/main/java/org/springframework/rewrite/gradle/Options.java b/spring-rewrite-commons-gradle/rewrite-gradle-parser/src/main/java/org/springframework/rewrite/gradle/Options.java new file mode 100644 index 0000000..44727cb --- /dev/null +++ b/spring-rewrite-commons-gradle/rewrite-gradle-parser/src/main/java/org/springframework/rewrite/gradle/Options.java @@ -0,0 +1,25 @@ +/* + * Copyright 2021 - 2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.rewrite.gradle; + +import org.openrewrite.style.NamedStyles; + +import java.util.List; + +public record Options(List exclusions, boolean logCompilationWarningsAndErrors, List plainTextMasks, + int sizeThresholdMb, List styles) { + +} diff --git a/spring-rewrite-commons-gradle/rewrite-gradle-parser/src/main/java/org/springframework/rewrite/gradle/ProjectParser.java b/spring-rewrite-commons-gradle/rewrite-gradle-parser/src/main/java/org/springframework/rewrite/gradle/ProjectParser.java new file mode 100644 index 0000000..aefa949 --- /dev/null +++ b/spring-rewrite-commons-gradle/rewrite-gradle-parser/src/main/java/org/springframework/rewrite/gradle/ProjectParser.java @@ -0,0 +1,631 @@ +/* + * Copyright 2021 - 2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.rewrite.gradle; + +import org.openrewrite.ExecutionContext; +import org.openrewrite.InMemoryExecutionContext; +import org.openrewrite.SourceFile; +import org.openrewrite.gradle.GradleParser; +import org.openrewrite.gradle.toolingapi.GradleSettings; +import org.springframework.rewrite.gradle.model.GradleProjectData; +import org.springframework.rewrite.gradle.model.JavaSourceSetData; +import org.springframework.rewrite.gradle.model.KotlinSourceSetData; +import org.openrewrite.groovy.GroovyParser; +import org.openrewrite.internal.StringUtils; +import org.openrewrite.internal.lang.Nullable; +import org.openrewrite.java.JavaParser; +import org.openrewrite.java.internal.JavaTypeCache; +import org.openrewrite.java.marker.JavaProject; +import org.openrewrite.java.marker.JavaSourceSet; +import org.openrewrite.java.marker.JavaVersion; +import org.openrewrite.kotlin.KotlinParser; +import org.openrewrite.marker.*; +import org.openrewrite.marker.ci.BuildEnvironment; +import org.openrewrite.polyglot.*; +import org.openrewrite.quark.QuarkParser; +import org.openrewrite.style.NamedStyles; +import org.openrewrite.text.PlainTextParser; +import org.openrewrite.tree.ParseError; +import org.slf4j.Logger; + +import java.io.*; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.PathMatcher; +import java.nio.file.Paths; +import java.util.*; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.Supplier; +import java.util.function.UnaryOperator; +import java.util.stream.Stream; + +import static java.util.Collections.*; +import static java.util.stream.Collectors.*; +import static org.openrewrite.PathUtils.separatorsToUnix; +import static org.openrewrite.Tree.randomId; + +@SuppressWarnings("unused") +public class ProjectParser { + + private static final String LOG_INDENT_INCREMENT = " "; + + private static final String GROOVY_PLUGIN = "org.gradle.api.plugins.GroovyPlugin"; + + private final Options options; + + private final Logger logger; + + private final AtomicBoolean firstWarningLogged = new AtomicBoolean(false); + + protected final Path baseDir; + + protected final GradleProjectData project; + + private final List sharedProvenance; + + public ProjectParser(GradleProjectData project, Options options, Logger logger) { + this.baseDir = repositoryRoot(project); + this.options = options; + this.project = project; + this.logger = logger; + + BuildEnvironment buildEnvironment = BuildEnvironment.build(System::getenv); + sharedProvenance = Stream + .of(buildEnvironment, gitProvenance(baseDir, buildEnvironment), OperatingSystemProvenance.current(), + new BuildTool(randomId(), BuildTool.Type.Gradle, project.getGradleVersion())) + .filter(Objects::nonNull) + .collect(toList()); + } + + /** + * Attempt to determine the root of the git repository for the given project. Many + * Gradle builds co-locate the build root with the git repository root, but that is + * not required. If no git repository can be located in any folder containing the + * build, the build root will be returned. + */ + static Path repositoryRoot(GradleProjectData project) { + Path buildRoot = project.getProjectDir().toPath(); + Path maybeBaseDir = buildRoot; + while (maybeBaseDir != null && !Files.exists(maybeBaseDir.resolve(".git"))) { + maybeBaseDir = maybeBaseDir.getParent(); + } + if (maybeBaseDir == null) { + return buildRoot; + } + return maybeBaseDir; + } + + @Nullable + private GitProvenance gitProvenance(Path baseDir, @Nullable BuildEnvironment buildEnvironment) { + try { + return GitProvenance.fromProjectDirectory(baseDir, buildEnvironment); + } + catch (Exception e) { + // Logging at a low level as this is unlikely to happen except in non-git + // projects, where it is expected + logger.debug("Unable to determine git provenance", e); + } + return null; + } + + // By accident, we were inconsistent with the names of these properties between this + // and the maven plugin + // Check all variants of the name, preferring more-fully-qualified names + @Nullable + private String getPropertyWithVariantNames(String property) { + String maybeProp = System.getProperty("rewrite." + property + "s"); + if (maybeProp == null) { + maybeProp = System.getProperty("rewrite." + property); + } + if (maybeProp == null) { + maybeProp = System.getProperty(property + "s"); + } + if (maybeProp == null) { + maybeProp = System.getProperty(property); + } + return maybeProp; + } + + public Collection listSources() { + // Use a sorted collection so that gradle input detection isn't thrown off by + // ordering + Set result = new TreeSet<>( + omniParser(emptySet()).acceptedPaths(baseDir, project.getProjectDir().toPath())); + for (JavaSourceSetData sourceSet : project.getJavaSourceSets()) { + sourceSet.getSources() + .stream() + .map(File::toPath) + .map(Path::toAbsolutePath) + .map(Path::normalize) + .forEach(result::add); + } + return result; + } + + public Stream parse(ExecutionContext ctx) { + Stream builder = Stream.of(); + Set alreadyParsed = new HashSet<>(); + if (project.isRootProject()) { + for (GradleProjectData subProject : project.getSubprojects()) { + builder = Stream.concat(builder, parse(subProject, alreadyParsed, ctx)); + } + } + builder = Stream.concat(builder, parse(project, alreadyParsed, ctx)); + + // log parse errors here at the end, so that we don't log parse errors for files + // that were excluded + return builder.map(this::logParseErrors); + } + + public Stream parse(GradleProjectData subproject, Set alreadyParsed, ExecutionContext ctx) { + String cliPort = System.getenv("MODERNE_CLI_PORT"); + try (ProgressBar progressBar = StringUtils.isBlank(cliPort) ? new NoopProgressBar() + : new RemoteProgressBarSender(Integer.parseInt(cliPort))) { + SourceFileStream sourceFileStream = SourceFileStream.build(subproject.getPath(), + projectName -> progressBar.intermediateResult(":" + projectName)); + + Collection exclusions = options.exclusions() + .stream() + .map(pattern -> subproject.getProjectDir().toPath().getFileSystem().getPathMatcher("glob:" + pattern)) + .collect(toList()); + if (isExcluded(exclusions, baseDir.relativize(subproject.getProjectDir().toPath()))) { + logger.info("Skipping project {} because it is excluded", subproject.getPath()); + return Stream.empty(); + } + + logger.info("Scanning sources in project {}", subproject.getPath()); + List styles = options.styles(); + logger.info("Using active styles {}", styles.stream().map(NamedStyles::getName).collect(toList())); + List sourceSets = subproject.getJavaSourceSets() + .stream() + .sorted(Comparator.comparingInt(sourceSet -> { + if ("main".equals(sourceSet.getName())) { + return 0; + } + else if ("test".equals(sourceSet.getName())) { + return 1; + } + else { + return 2; + } + })) + .toList(); + List projectProvenance; + if (sourceSets.isEmpty()) { + projectProvenance = sharedProvenance; + } + else { + projectProvenance = new ArrayList<>(sharedProvenance); + projectProvenance.add(new JavaProject(randomId(), subproject.getName(), new JavaProject.Publication( + subproject.getGroup(), subproject.getName(), subproject.getVersion()))); + } + + if (subproject.isMultiPlatformKotlinProject()) { + sourceFileStream = sourceFileStream + .concat(parseMultiplatformKotlinProject(subproject, exclusions, alreadyParsed, ctx)); + } + + Set sourceDirs = new HashSet<>(); + for (JavaSourceSetData sourceSet : sourceSets) { + Stream sourceSetSourceFiles = Stream.of(); + int sourceSetSize = 0; + + JavaTypeCache javaTypeCache = new JavaTypeCache(); + JavaVersion javaVersion = new JavaVersion(randomId(), sourceSet.getJavaVersionData().getCreatedBy(), + sourceSet.getJavaVersionData().getVmVendor(), + sourceSet.getJavaVersionData().getSourceCompatibility(), + sourceSet.getJavaVersionData().getTargetCompatibility()); + + List unparsedSources = sourceSet.getSources() + .stream() + .filter(it -> it.exists() && !alreadyParsed.contains(it.toPath())) + .flatMap(sourceDir -> { + try { + return Files.walk(sourceDir.toPath()); + } + catch (IOException e) { + throw new UncheckedIOException(e); + } + }) + .filter(Files::isRegularFile) + .map(Path::toAbsolutePath) + .map(Path::normalize) + .distinct() + .toList(); + List javaPaths = unparsedSources.stream() + .filter(it -> it.toString().endsWith(".java") && !alreadyParsed.contains(it)) + .collect(toList()); + + Collection implementationClasspath = sourceSet.getImplementationClasspath(); + // The implementation configuration doesn't include build/source + // directories from project dependencies + // So mash it and our rewriteImplementation together to get everything + List dependencyPaths = Stream + .concat(implementationClasspath.stream(), sourceSet.getCompileClasspath().stream()) + .map(File::toPath) + .map(Path::toAbsolutePath) + .map(Path::normalize) + .distinct() + .collect(toList()); + + if (!javaPaths.isEmpty()) { + alreadyParsed.addAll(javaPaths); + Stream cus = Stream + .of((Supplier) () -> JavaParser.fromJavaVersion() + .classpath(dependencyPaths) + .styles(options.styles()) + .typeCache(javaTypeCache) + .logCompilationWarningsAndErrors(options.logCompilationWarningsAndErrors()) + .build()) + .map(Supplier::get) + .flatMap(jp -> jp.parse(javaPaths, baseDir, ctx)) + .map(cu -> { + if (isExcluded(exclusions, cu.getSourcePath()) || cu.getSourcePath() + .startsWith(baseDir.relativize(subproject.getBuildDir().toPath()))) { + return null; + } + return cu; + }) + .filter(Objects::nonNull) + .map(it -> it.withMarkers(it.getMarkers().add(javaVersion))); + sourceSetSourceFiles = Stream.concat(sourceSetSourceFiles, cus); + sourceSetSize += javaPaths.size(); + logger.info("Scanned {} Java sources in {}/{}", javaPaths.size(), subproject.getPath(), + sourceSet.getName()); + } + + if (subproject.getPlugins().stream().anyMatch(gpd -> "org.jetbrains.kotlin.jvm".equals(gpd.getId()))) { + String excludedProtosPath = subproject.getProjectDir().getPath() + "/protos/build/generated"; + List kotlinPaths = unparsedSources.stream() + .filter(it -> !it.toString().startsWith(excludedProtosPath)) + .filter(it -> it.toString().endsWith(".kt")) + .collect(toList()); + + if (!kotlinPaths.isEmpty()) { + alreadyParsed.addAll(kotlinPaths); + Stream cus = Stream + .of((Supplier) () -> KotlinParser.builder() + .classpath(dependencyPaths) + .styles(options.styles()) + .typeCache(javaTypeCache) + .logCompilationWarningsAndErrors(options.logCompilationWarningsAndErrors()) + .build()) + .map(Supplier::get) + .flatMap(kp -> kp.parse(kotlinPaths, baseDir, ctx)) + .map(cu -> { + if (isExcluded(exclusions, cu.getSourcePath())) { + return null; + } + return cu; + }) + .filter(Objects::nonNull) + .map(it -> it.withMarkers(it.getMarkers().add(javaVersion))); + sourceSetSourceFiles = Stream.concat(sourceSetSourceFiles, cus); + sourceSetSize += kotlinPaths.size(); + logger.info("Scanned {} Kotlin sources in {}/{}", kotlinPaths.size(), subproject.getPath(), + sourceSet.getName()); + } + } + if (subproject.getPlugins() + .stream() + .anyMatch(gpd -> gpd.getFullyQualifiedClassName().startsWith(GROOVY_PLUGIN))) { + List groovyPaths = unparsedSources.stream() + .filter(it -> it.toString().endsWith(".groovy")) + .collect(toList()); + + if (!groovyPaths.isEmpty()) { + // Groovy sources are aware of java types that are intermixed in + // the same directory/sourceSet + // Include the build directory containing class files so these + // definitions are available + List dependenciesWithBuildDirs = Stream + .concat(dependencyPaths.stream(), sourceSet.getClassesDirs().stream().map(File::toPath)) + .collect(toList()); + + alreadyParsed.addAll(groovyPaths); + + Stream cus = Stream + .of((Supplier) () -> GroovyParser.builder() + .classpath(dependenciesWithBuildDirs) + .styles(options.styles()) + .typeCache(javaTypeCache) + .logCompilationWarningsAndErrors(false) + .build()) + .map(Supplier::get) + .flatMap(gp -> gp.parse(groovyPaths, baseDir, ctx)) + .map(cu -> { + if (isExcluded(exclusions, cu.getSourcePath())) { + return null; + } + return cu; + }) + .filter(Objects::nonNull) + .map(it -> it.withMarkers(it.getMarkers().add(javaVersion))); + sourceSetSourceFiles = Stream.concat(sourceSetSourceFiles, cus); + sourceSetSize += groovyPaths.size(); + logger.info("Scanned {} Groovy sources in {}/{}", groovyPaths.size(), subproject.getPath(), + sourceSet.getName()); + } + } + + for (File resourcesDir : sourceSet.getSourceDirectories()) { + if (resourcesDir.exists() && !alreadyParsed.contains(resourcesDir.toPath())) { + OmniParser omniParser = omniParser(alreadyParsed); + List accepted = omniParser.acceptedPaths(baseDir, resourcesDir.toPath()); + sourceSetSourceFiles = Stream.concat(sourceSetSourceFiles, + omniParser.parse(accepted, baseDir, new InMemoryExecutionContext())); + alreadyParsed.addAll(accepted); + sourceSetSize += accepted.size(); + } + } + + JavaSourceSet sourceSetProvenance = JavaSourceSet.build(sourceSet.getName(), dependencyPaths, + javaTypeCache, false); + sourceFileStream = sourceFileStream.concat(sourceSetSourceFiles.map(addProvenance(sourceSetProvenance)), + sourceSetSize); + // Some source sets get misconfigured to have the same directories as + // other source sets + // This causes duplicate source files to be parsed, so once a source set + // has been parsed exclude it from future parsing + for (File file : sourceSet.getSourceDirectories()) { + alreadyParsed.add(file.toPath()); + } + } + SourceFileStream gradleFiles = parseGradleFiles(exclusions, alreadyParsed, ctx); + sourceFileStream = sourceFileStream.concat(gradleFiles, gradleFiles.size()); + + SourceFileStream nonProjectResources = parseNonProjectResources(subproject, alreadyParsed, ctx, + projectProvenance, sourceFileStream); + sourceFileStream = sourceFileStream.concat(nonProjectResources, nonProjectResources.size()); + + progressBar.setMax(sourceFileStream.size()); + return sourceFileStream.map(addProvenance(projectProvenance)).peek(it -> progressBar.step()); + } + catch (Exception e) { + throw new RuntimeException(e); + } + } + + private GradleParser gradleParser() { + List settingsClasspath = project.getSettingsClasspath().stream().map(File::toPath).collect(toList()); + List buildscriptClasspath = project.getBuildscriptClasspath() + .stream() + .map(File::toPath) + .collect(toList()); + + return GradleParser.builder() + .groovyParser(GroovyParser.builder() + .typeCache(new JavaTypeCache()) + .styles(options.styles()) + .logCompilationWarningsAndErrors(false)) + .buildscriptClasspath(buildscriptClasspath) + .settingsClasspath(settingsClasspath) + .build(); + } + + private SourceFileStream parseGradleFiles(Collection exclusions, Set alreadyParsed, + ExecutionContext ctx) { + Stream sourceFiles = Stream.empty(); + int gradleFileCount = 0; + + GradleParser gradleParser = null; + if (project.getBuildscriptFile() != null) { + File buildGradleFile = project.getBuildscriptFile(); + Path buildScriptPath = baseDir.relativize(buildGradleFile.toPath()); + if (!isExcluded(exclusions, buildScriptPath) && buildGradleFile.exists()) { + alreadyParsed.add(buildScriptPath); + if (buildScriptPath.toString().endsWith(".gradle")) { + gradleParser = gradleParser(); + sourceFiles = gradleParser.parse(singleton(buildGradleFile.toPath()), baseDir, ctx); + } + else { + sourceFiles = PlainTextParser.builder() + .build() + .parse(singleton(buildGradleFile.toPath()), baseDir, ctx); + } + gradleFileCount++; + sourceFiles = sourceFiles.map(sourceFile -> sourceFile.withMarkers(sourceFile.getMarkers() + .add(org.openrewrite.gradle.marker.GradleProject.fromToolingModel(project)))); + alreadyParsed.add(project.getBuildscriptFile().toPath()); + } + } + + if (project.isRootProject()) { + File settingsGradleFile = new File(project.getProjectDir(), "settings.gradle"); + File settingsGradleKtsFile = new File(project.getProjectDir(), "settings.gradle.kts"); + GradleSettings gs = project.getGradleSettings(); + if (settingsGradleFile.exists()) { + Path settingsPath = baseDir.relativize(settingsGradleFile.toPath()); + if (gradleParser == null) { + gradleParser = gradleParser(); + } + if (!isExcluded(exclusions, settingsPath)) { + sourceFiles = Stream.concat(sourceFiles, + gradleParser.parse(singleton(settingsGradleFile.toPath()), baseDir, ctx).map(sourceFile -> { + if (gs == null) { + return sourceFile; + } + return sourceFile.withMarkers(sourceFile.getMarkers() + .add(org.openrewrite.gradle.marker.GradleSettings.fromToolingModel(gs))); + })); + gradleFileCount++; + } + alreadyParsed.add(settingsGradleFile.toPath()); + } + else if (settingsGradleKtsFile.exists()) { + Path settingsPath = baseDir.relativize(settingsGradleKtsFile.toPath()); + if (!isExcluded(exclusions, settingsPath)) { + sourceFiles = Stream.concat(sourceFiles, + PlainTextParser.builder() + .build() + .parse(singleton(settingsGradleKtsFile.toPath()), baseDir, ctx) + .map(sourceFile -> { + if (gs == null) { + return sourceFile; + } + return sourceFile.withMarkers(sourceFile.getMarkers() + .add(org.openrewrite.gradle.marker.GradleSettings.fromToolingModel(gs))); + })); + gradleFileCount++; + } + alreadyParsed.add(settingsGradleKtsFile.toPath()); + } + } + + return SourceFileStream.build("", s -> { + }).concat(sourceFiles, gradleFileCount); + } + + protected SourceFileStream parseNonProjectResources(GradleProjectData subproject, Set alreadyParsed, + ExecutionContext ctx, List projectProvenance, Stream sourceFiles) { + // Collect any additional yaml/properties/xml files that are NOT already in a + // source set. + OmniParser omniParser = omniParser(alreadyParsed); + List accepted = omniParser.acceptedPaths(baseDir, subproject.getProjectDir().toPath()); + return SourceFileStream.build("", s -> { + }).concat(omniParser.parse(accepted, baseDir, ctx), accepted.size()); + } + + private OmniParser omniParser(Set alreadyParsed) { + return OmniParser + .builder(OmniParser.defaultResourceParsers(), + PlainTextParser.builder().plainTextMasks(baseDir, options.plainTextMasks()).build(), + QuarkParser.builder().build()) + .exclusionMatchers(pathMatchers(baseDir, mergeExclusions(project, baseDir, options))) + .exclusions(alreadyParsed) + .sizeThresholdMb(options.sizeThresholdMb()) + .build(); + } + + private static Collection mergeExclusions(GradleProjectData project, Path baseDir, Options options) { + return Stream + .concat(project.getSubprojects() + .stream() + .map(subproject -> separatorsToUnix( + baseDir.relativize(subproject.getProjectDir().toPath()).toString())), + options.exclusions().stream()) + .collect(toList()); + } + + private Collection pathMatchers(Path basePath, Collection pathExpressions) { + return pathExpressions.stream() + .map(o -> basePath.getFileSystem().getPathMatcher("glob:" + o)) + .collect(toList()); + } + + private SourceFileStream parseMultiplatformKotlinProject(GradleProjectData subproject, + Collection exclusions, Set alreadyParsed, ExecutionContext ctx) { + SourceFileStream sourceFileStream = SourceFileStream.build(subproject.getPath(), s -> { + }); + for (KotlinSourceSetData sourceSet : project.getKotlinSourceSets()) { + List kotlinPaths = sourceSet.getKotlin() + .stream() + .filter(it -> it.isFile() && it.getName().endsWith(".kt")) + .map(File::toPath) + .map(Path::toAbsolutePath) + .map(Path::normalize) + .collect(toList()); + + // The implementation configuration doesn't include build/source directories + // from project dependencies + // So mash it and our rewriteImplementation together to get everything + List dependencyPaths = Stream + .concat(sourceSet.getImplementationClasspath().stream(), sourceSet.getCompileClasspath().stream()) + .map(File::toPath) + .map(Path::toAbsolutePath) + .map(Path::normalize) + .distinct() + .collect(toList()); + + if (!kotlinPaths.isEmpty()) { + JavaTypeCache javaTypeCache = new JavaTypeCache(); + KotlinParser kp = KotlinParser.builder() + .classpath(dependencyPaths) + .styles(options.styles()) + .typeCache(javaTypeCache) + .logCompilationWarningsAndErrors(options.logCompilationWarningsAndErrors()) + .build(); + + Stream cus = kp.parse(kotlinPaths, baseDir, ctx); + alreadyParsed.addAll(kotlinPaths); + cus = cus.map(cu -> { + if (isExcluded(exclusions, cu.getSourcePath())) { + return null; + } + return cu; + }).filter(Objects::nonNull); + JavaSourceSet sourceSetProvenance = JavaSourceSet.build(sourceSet.getName(), dependencyPaths, + javaTypeCache, false); + + sourceFileStream = sourceFileStream.concat(cus.map(addProvenance(sourceSetProvenance)), + kotlinPaths.size()); + logger.info("Scanned {} Kotlin sources in {}/{}", kotlinPaths.size(), subproject.getPath(), + sourceSet.getName()); + } + } + + return sourceFileStream; + } + + private SourceFile logParseErrors(SourceFile source) { + if (source instanceof ParseError) { + if (firstWarningLogged.compareAndSet(false, true)) { + logger.warn("There were problems parsing some source files, run with --info to see full stack traces"); + } + logger.warn("There were problems parsing " + source.getSourcePath()); + } + return source; + } + + private boolean isExcluded(Collection exclusions, Path path) { + for (PathMatcher excluded : exclusions) { + if (excluded.matches(path)) { + return true; + } + } + // PathMather will not evaluate the path "build.gradle" to be matched by the + // pattern "**/build.gradle" + // This is counter-intuitive for most users and would otherwise require separate + // exclusions for files at the root and files in subdirectories + if (!path.isAbsolute() && !path.startsWith(File.separator)) { + return isExcluded(exclusions, Paths.get("/" + path)); + } + return false; + } + + private UnaryOperator addProvenance(List projectProvenance) { + return s -> { + Markers m = s.getMarkers(); + for (Marker marker : projectProvenance) { + m = m.addIfAbsent(marker); + } + return s.withMarkers(m); + }; + } + + private UnaryOperator addProvenance(Marker sourceSet) { + return s -> { + Markers m = s.getMarkers(); + m = m.addIfAbsent(sourceSet); + return s.withMarkers(m); + }; + } + + public Path getBaseDir() { + return baseDir; + } + +} diff --git a/spring-rewrite-commons-gradle/rewrite-gradle-parser/src/test/java/org/springframework/rewrite/gradle/ProjectParserTest.java b/spring-rewrite-commons-gradle/rewrite-gradle-parser/src/test/java/org/springframework/rewrite/gradle/ProjectParserTest.java new file mode 100644 index 0000000..ecbb1f3 --- /dev/null +++ b/spring-rewrite-commons-gradle/rewrite-gradle-parser/src/test/java/org/springframework/rewrite/gradle/ProjectParserTest.java @@ -0,0 +1,112 @@ +/* + * Copyright 2021 - 2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.rewrite.gradle; + +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; +import org.openrewrite.InMemoryExecutionContext; +import org.openrewrite.SourceFile; +import org.openrewrite.style.NamedStyles; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.rewrite.gradle.model.GradleProjectData; +import org.springframework.rewrite.gradle.model.SpringRewriteModelBuilder; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.net.URL; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Collections; +import java.util.List; +import java.util.zip.ZipEntry; +import java.util.zip.ZipInputStream; + +import static org.assertj.core.api.Assertions.assertThat; + +public class ProjectParserTest { + + private static final Logger log = LoggerFactory.getLogger(ProjectParserTest.class); + + private static final Options OPTIONS = new Options(Collections.emptyList(), false, Collections.emptyList(), + Integer.MAX_VALUE, Collections.emptyList()); + + private Path downloadPetclinic(Path dir) throws Exception { + unzip(new URL( + "https://github.com/spring-projects/spring-petclinic/archive/0aa3adb56f500c41564411c32cd301affe284ecc.zip"), + dir); + return Files.list(dir) + .filter(Files::isDirectory) + .filter(p -> p.getFileName().toString().startsWith("spring-petclinic-")) + .findFirst() + .orElseThrow(); + } + + private static void unzip(URL url, Path destDir) throws IOException { + File dir = destDir.toFile(); + // create output directory if it doesn't exist + if (!dir.exists()) + dir.mkdirs(); + InputStream is; + // buffer for read and write data to file + byte[] buffer = new byte[1024]; + try { + is = url.openStream(); + ZipInputStream zis = new ZipInputStream(is); + ZipEntry ze = zis.getNextEntry(); + while (ze != null) { + if (!ze.isDirectory()) { + String fileName = ze.getName(); + File newFile = new File(destDir + File.separator + fileName); + System.out.println("Unzipping to " + newFile.getAbsolutePath()); + // create directories for sub directories in zip + new File(newFile.getParent()).mkdirs(); + FileOutputStream fos = new FileOutputStream(newFile); + int len; + while ((len = zis.read(buffer)) > 0) { + fos.write(buffer, 0, len); + } + fos.close(); + } + // close this ZipEntry + zis.closeEntry(); + ze = zis.getNextEntry(); + } + // close last ZipEntry + zis.closeEntry(); + zis.close(); + is.close(); + } + catch (IOException e) { + throw e; + } + } + + @Test + void sanity(@TempDir Path dir) throws Exception { + Path petClinic = downloadPetclinic(dir.resolve("petclinic")); + GradleProjectData gp = SpringRewriteModelBuilder.forProjectDirectory(GradleProjectData.class, + petClinic.toFile(), petClinic.resolve("build.gradle").toFile()); + List sources = new ProjectParser(gp, OPTIONS, log) + .parse(new InMemoryExecutionContext(t -> Assertions.fail("Parser Error", t))) + .toList(); + assertThat(sources.size()).isEqualTo(114); + } + +} diff --git a/spring-rewrite-commons-gradle/rewrite-gradle-plugin/pom.xml b/spring-rewrite-commons-gradle/rewrite-gradle-plugin/pom.xml new file mode 100644 index 0000000..ba91fdd --- /dev/null +++ b/spring-rewrite-commons-gradle/rewrite-gradle-plugin/pom.xml @@ -0,0 +1,66 @@ + + + 4.0.0 + + org.springframework.rewrite + spring-rewrite-commons-gradle + 0.1.0-SNAPSHOT + + + rewrite-gradle-plugin + + + 17 + 17 + UTF-8 + 1.18.30 + + + + + org.springframework.rewrite + rewrite-gradle-model + 0.1.0-SNAPSHOT + + + org.projectlombok + lombok + ${lombok.version} + provided + + + dev.gradleplugins + gradle-api + ${gradle.version} + provided + + + javax.inject + javax.inject + 1 + compile + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.11.0 + + + + org.projectlombok + lombok + ${lombok.version} + + + + + + + + \ No newline at end of file diff --git a/spring-rewrite-commons-gradle/rewrite-gradle-plugin/src/main/java/org/springframework/rewrite/gradle/plugin/SpringRewritePlugin.java b/spring-rewrite-commons-gradle/rewrite-gradle-plugin/src/main/java/org/springframework/rewrite/gradle/plugin/SpringRewritePlugin.java new file mode 100644 index 0000000..3e2b7fb --- /dev/null +++ b/spring-rewrite-commons-gradle/rewrite-gradle-plugin/src/main/java/org/springframework/rewrite/gradle/plugin/SpringRewritePlugin.java @@ -0,0 +1,55 @@ +/* + * Copyright 2021 - 2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.rewrite.gradle.plugin; + +import org.gradle.api.Plugin; +import org.gradle.api.Project; +import org.gradle.tooling.provider.model.ToolingModelBuilder; +import org.gradle.tooling.provider.model.ToolingModelBuilderRegistry; +import org.springframework.rewrite.gradle.model.GradleProjectData; +import org.springframework.rewrite.gradle.plugin.model.GradleToolingApiProjectBuilder; + +import javax.inject.Inject; + +public class SpringRewritePlugin implements Plugin { + + private final ToolingModelBuilderRegistry registry; + + @Inject + public SpringRewritePlugin(ToolingModelBuilderRegistry registry) { + this.registry = registry; + } + + @Override + public void apply(Project project) { + registry.register(new GradleProjectDataBuilder()); + } + + private static class GradleProjectDataBuilder implements ToolingModelBuilder { + + @Override + public boolean canBuild(String modelName) { + return modelName.equals(GradleProjectData.class.getName()); + } + + @Override + public Object buildAll(String modelName, Project project) { + return GradleToolingApiProjectBuilder.createProjectData(project); + } + + } + +} diff --git a/spring-rewrite-commons-gradle/rewrite-gradle-plugin/src/main/java/org/springframework/rewrite/gradle/plugin/model/GradleProjectDataImpl.java b/spring-rewrite-commons-gradle/rewrite-gradle-plugin/src/main/java/org/springframework/rewrite/gradle/plugin/model/GradleProjectDataImpl.java new file mode 100644 index 0000000..6a5e7a2 --- /dev/null +++ b/spring-rewrite-commons-gradle/rewrite-gradle-plugin/src/main/java/org/springframework/rewrite/gradle/plugin/model/GradleProjectDataImpl.java @@ -0,0 +1,272 @@ +/* + * Copyright 2021 - 2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.rewrite.gradle.plugin.model; + +import lombok.AllArgsConstructor; +import lombok.Value; +import org.gradle.api.NamedDomainObjectContainer; +import org.gradle.api.Project; +import org.gradle.api.artifacts.Configuration; +import org.gradle.api.file.SourceDirectorySet; +import org.gradle.api.initialization.Settings; +import org.gradle.api.logging.Logger; +import org.gradle.api.logging.Logging; +import org.gradle.api.plugins.JavaPluginConvention; +import org.gradle.api.tasks.SourceSet; +import org.gradle.api.tasks.compile.JavaCompile; +import org.gradle.invocation.DefaultGradle; +import org.gradle.util.GradleVersion; +import org.openrewrite.gradle.toolingapi.*; +import org.springframework.rewrite.gradle.model.GradleProjectData; +import org.springframework.rewrite.gradle.model.JavaSourceSetData; +import org.springframework.rewrite.gradle.model.KotlinSourceSetData; + +import java.io.File; +import java.io.Serializable; +import java.util.*; +import java.util.stream.Collectors; + +import static java.util.Collections.emptyList; + +@Value +@AllArgsConstructor +class GradleProjectDataImpl implements GradleProjectData, Serializable { + + private static final Logger logger = Logging.getLogger(GradleProjectDataImpl.class); + + private static Class[] SUPPORTED_GRADLE_PROPERTY_VALUE_TYPES = new Class[] { Number.class, Boolean.class, + String.class, Character.class }; + + String name; + + String path; + + String group; + + String version; + + List plugins; + + List mavenRepositories; + + List mavenPluginRepositories; + + Map nameToConfiguration; + + GradleSettings gradleSettings; + + String gradleVersion; + + boolean rootProject; + + File rootProjectDir; + + Collection subprojects; + + File projectDir; + + File buildDir; + + File buildscriptFile; + + Map properties; + + List javaSourceSets; + + boolean multiPlatformKotlinProject; + + List kotlinSourceSets; + + Collection buildscriptClasspath; + + Collection settingsClasspath; + + static GradleProjectDataImpl from(Project project) { + GradleProject toolingRewriiteGradleProject = GradleToolingApiProjectBuilder.gradleProject(project); + return new GradleProjectDataImpl(project.getName(), project.getPath(), project.getGroup().toString(), + project.getVersion().toString(), + GradleToolingApiProjectBuilder.pluginDescriptors(project.getPluginManager()), + GradleToolingApiProjectBuilder.mapRepositories(project.getRepositories()), + GradleToolingApiProjectBuilder.pluginMavenRepos(project), + GradleToolingApiProjectBuilder.dependencyConfigurations(project.getConfigurations()), + GradleVersion.current().compareTo(GradleVersion.version("4.4")) >= 0 ? GradleToolingApiSettingsBuilder + .gradleSettings(((DefaultGradle) project.getGradle()).getSettings()) : null, + project.getGradle().getGradleVersion(), project == project.getRootProject(), + project.getRootProject().getProjectDir(), subprojects(project.getSubprojects()), + project.getProjectDir(), project.getBuildDir(), project.getBuildscript().getSourceFile(), + properties(project.getProperties()), javaSourceSets(project), isMultiPlatformKotlinProject(project), + kotlinSourceSets(project), + project.getBuildscript().getConfigurations().getByName("classpath").resolve(), + settingsClasspath(project)); + } + + private static Collection subprojects(Collection subprojects) { + List sub = new ArrayList(subprojects.size()); + for (Project s : subprojects) { + sub.add(from(s)); + } + return sub; + } + + private static Map properties(Map props) { + return props.entrySet() + .stream() + .filter(e -> Arrays.stream(SUPPORTED_GRADLE_PROPERTY_VALUE_TYPES).anyMatch(c -> c.isInstance(e.getValue()))) + .collect(Collectors.toMap(e -> e.getKey(), e -> e.getValue())); + } + + private static List javaSourceSets(Project project) { + JavaPluginConvention javaConvention = (JavaPluginConvention) project.getConvention() + .findPlugin(JavaPluginConvention.class); + if (javaConvention == null) { + return Collections.emptyList(); + } + else { + List sourceSetData = new ArrayList(javaConvention.getSourceSets().size()); + for (SourceSet sourceSet : javaConvention.getSourceSets()) { + sourceSetData.add(new JavaSourceSetDataImpl(sourceSet.getName(), sourceSet.getAllSource().getFiles(), + sourceSet.getResources().getSourceDirectories().getFiles(), sourceSet.getAllJava().getFiles(), + sourceSet.getOutput().getClassesDirs().getFiles(), sourceSet.getCompileClasspath().getFiles(), + javaSourceSetImplementationClasspath(project, sourceSet), + sourceSetJavaVersion(project, sourceSet))); + } + return sourceSetData; + } + } + + private static Collection javaSourceSetImplementationClasspath(Project project, SourceSet sourceSet) { + // classpath doesn't include the transitive dependencies of the implementation + // configuration + // These aren't needed for compilation, but we want them so recipes have access to + // comprehensive type information + // The implementation configuration isn't resolvable, so we need a new + // configuration that extends from it + Configuration implementation = project.getConfigurations() + .getByName(sourceSet.getImplementationConfigurationName()); + Configuration rewriteImplementation = project.getConfigurations() + .maybeCreate("rewrite" + sourceSet.getImplementationConfigurationName()); + rewriteImplementation.extendsFrom(new Configuration[] { implementation }); + + try { + return rewriteImplementation.resolve(); + } + catch (Exception e) { + return Collections.emptySet(); + } + } + + private static JavaVersionDataImpl sourceSetJavaVersion(Project project, SourceSet sourceSet) { + final JavaCompile javaCompileTask = (JavaCompile) project.getTasks() + .getByName(sourceSet.getCompileJavaTaskName()); + return new JavaVersionDataImpl(System.getProperty("java.runtime.version"), System.getProperty("java.vm.vendor"), + javaCompileTask.getSourceCompatibility(), javaCompileTask.getTargetCompatibility()); + } + + private static boolean isMultiPlatformKotlinProject(Project project) { + try { + return project.getPlugins().hasPlugin("org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension") + || project.getExtensions().findByName("kotlin") != null && project.getExtensions() + .findByName("kotlin") + .getClass() + .getCanonicalName() + .startsWith("org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension"); + } + catch (Throwable t) { + return false; + } + } + + private static List kotlinSourceSets(Project project) { + NamedDomainObjectContainer sourceSets; + try { + Object kotlinExtension = project.getExtensions().getByName("kotlin"); + Class clazz = kotlinExtension.getClass() + .getClassLoader() + .loadClass("org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension"); + sourceSets = (NamedDomainObjectContainer) clazz.getMethod("getSourceSets").invoke(kotlinExtension); + } + catch (Exception e) { + return Collections.emptyList(); + } + + SortedSet sourceSetNames; + try { + sourceSetNames = (SortedSet) sourceSets.getClass().getMethod("getNames").invoke(sourceSets); + } + catch (Exception e) { + return Collections.emptyList(); + } + + List kotlinSourceSetData = new ArrayList(sourceSetNames.size()); + + for (String sourceSetName : sourceSetNames) { + try { + Object sourceSet = sourceSets.getClass() + .getMethod("getByName", String.class) + .invoke(sourceSets, sourceSetName); + final SourceDirectorySet kotlinDirectorySet = (SourceDirectorySet) sourceSet.getClass() + .getMethod("getKotlin") + .invoke(sourceSet); + String implementationName = (String) sourceSet.getClass() + .getMethod("getImplementationConfigurationName") + .invoke(sourceSet); + Configuration implementation = project.getConfigurations().getByName(implementationName); + Configuration rewriteImplementation = (Configuration) project.getConfigurations() + .maybeCreate("rewrite" + implementationName); + rewriteImplementation.extendsFrom(new Configuration[] { implementation }); + + Set implementationClasspath; + try { + implementationClasspath = rewriteImplementation.resolve(); + } + catch (Exception e) { + implementationClasspath = Collections.emptySet(); + } + + String compileName = (String) sourceSet.getClass() + .getMethod("getCompileOnlyConfigurationName") + .invoke(sourceSet); + Configuration compileOnly = project.getConfigurations().getByName(compileName); + Configuration rewriteCompileOnly = project.getConfigurations().maybeCreate("rewrite" + compileName); + rewriteCompileOnly.setCanBeResolved(true); + rewriteCompileOnly.extendsFrom(new Configuration[] { compileOnly }); + final Set compClasspath = rewriteCompileOnly.getFiles(); + kotlinSourceSetData.add(new KotlinSourceSetDataImpl(sourceSetName, kotlinDirectorySet.getFiles(), + compClasspath, implementationClasspath)); + } + catch (Exception e) { + logger.warn("Failed to resolve sourceSet from {}:{}. Some type information may be incomplete", + project.getPath(), sourceSetName); + } + } + + return kotlinSourceSetData; + } + + private static Collection settingsClasspath(Project project) { + if (GradleVersion.current().compareTo(GradleVersion.version("4.4")) >= 0) { + try { + Settings settings = ((DefaultGradle) project.getGradle()).getSettings(); + return settings.getBuildscript().getConfigurations().getByName("classpath").resolve(); + } + catch (IllegalStateException e) { + // ignore - return empty list + } + } + return emptyList(); + } + +} diff --git a/spring-rewrite-commons-gradle/rewrite-gradle-plugin/src/main/java/org/springframework/rewrite/gradle/plugin/model/GradleToolingApiProjectBuilder.java b/spring-rewrite-commons-gradle/rewrite-gradle-plugin/src/main/java/org/springframework/rewrite/gradle/plugin/model/GradleToolingApiProjectBuilder.java new file mode 100644 index 0000000..0328b9e --- /dev/null +++ b/spring-rewrite-commons-gradle/rewrite-gradle-plugin/src/main/java/org/springframework/rewrite/gradle/plugin/model/GradleToolingApiProjectBuilder.java @@ -0,0 +1,483 @@ +/* + * Copyright 2021 - 2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.rewrite.gradle.plugin.model; + +import lombok.*; +import org.gradle.api.Project; +import org.gradle.api.artifacts.Configuration; +import org.gradle.api.artifacts.ConfigurationContainer; +import org.gradle.api.artifacts.ResolvedConfiguration; +import org.gradle.api.artifacts.repositories.ArtifactRepository; +import org.gradle.api.artifacts.repositories.MavenArtifactRepository; +import org.gradle.api.initialization.Settings; +import org.gradle.api.internal.plugins.PluginManagerInternal; +import org.gradle.api.plugins.PluginManager; +import org.gradle.invocation.DefaultGradle; +import org.gradle.plugin.use.PluginId; +import org.gradle.util.GradleVersion; +import org.openrewrite.gradle.toolingapi.*; +import org.springframework.rewrite.gradle.model.GradleProjectData; + +import java.io.Serializable; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; +import java.util.stream.Collectors; + +import static java.util.Collections.emptyList; +import static java.util.stream.Collectors.toList; + +public class GradleToolingApiProjectBuilder { + + @Builder + @Getter + static class MavenRepositoryImpl implements MavenRepository, Serializable { + + String id; + + String uri; + + String releases; + + String snapshots; + + boolean knownToExist; + + String username; + + String password; + + Boolean DeriveMetadataIfMissing; + + } + + @AllArgsConstructor + @Getter + static class GradlePluginDescriptorImpl implements GradlePluginDescriptor, Serializable { + + String fullyQualifiedClassName; + + String id; + + } + + @AllArgsConstructor + @Getter + @EqualsAndHashCode + static class GroupArtifactVersionImpl implements GroupArtifactVersion, Serializable { + + String groupId; + + String artifactId; + + String version; + + } + + @AllArgsConstructor + @Getter + @EqualsAndHashCode + static class GroupArtifactImpl implements GroupArtifact, Serializable { + + String groupId; + + String artifactId; + + } + + @AllArgsConstructor + @Getter + @Builder + @EqualsAndHashCode + static class DependencyImpl implements Dependency, Serializable { + + GroupArtifactVersion gav; + + String classifier; + + String type; + + String scope; + + List exclusions; + + String optional; + + } + + @AllArgsConstructor + @Getter + @EqualsAndHashCode + static class ResolvedGroupArtifactVersionImpl implements ResolvedGroupArtifactVersion, Serializable { + + String artifactId; + + String groupId; + + String version; + + String datedSnapshotVersion; + + } + + @AllArgsConstructor + @Builder + @Getter + @EqualsAndHashCode + static class ResolvedDependencyImpl implements ResolvedDependency, Serializable { + + MavenRepositoryImpl repository; + + ResolvedGroupArtifactVersionImpl gav; + + DependencyImpl requested; + + List dependencies; + + int depth; + + } + + @AllArgsConstructor + @Getter + static class GradleDependencyConfigurationImpl implements GradleDependencyConfiguration, Serializable { + + String name; + + String description; + + boolean transitive; + + boolean canBeConsumed; + + boolean canBeResolved; + + List extendsFrom; + + List requested; + + List resolved; + + } + + @AllArgsConstructor + @Getter + static class GradleProjectImpl implements GradleProject, Serializable { + + String name; + + String path; + + List plugins; + + List mavenRepositories; + + List mavenPluginRepositories; + + Map nameToConfiguration; + + } + + static MavenRepositoryImpl GRADLE_PLUGIN_PORTAL = MavenRepositoryImpl.builder() + .id("Gradle Central Plugin Repository") + .uri("https://plugins.gradle.org/m2") + .releases(String.valueOf(true)) + .snapshots(String.valueOf(true)) + .build(); + + public static GradleProject gradleProject(Project project) { + return new GradleProjectImpl(project.getName(), project.getPath(), + pluginDescriptors(project.getPluginManager()), mapRepositories(project.getRepositories()), + pluginMavenRepos(project), dependencyConfigurations(project.getConfigurations())); + } + + static List pluginMavenRepos(Project project) { + Set pluginRepositories = new HashSet<>(); + if (GradleVersion.current().compareTo(GradleVersion.version("4.4")) >= 0) { + Settings settings = ((DefaultGradle) project.getGradle()).getSettings(); + pluginRepositories.addAll(mapRepositories(settings.getPluginManagement().getRepositories())); + pluginRepositories.addAll(mapRepositories(settings.getBuildscript().getRepositories())); + } + pluginRepositories.addAll(mapRepositories(project.getBuildscript().getRepositories())); + if (pluginRepositories.isEmpty()) { + pluginRepositories.add(GRADLE_PLUGIN_PORTAL); + } + + return new ArrayList<>(pluginRepositories); + } + + static List mapRepositories(List repositories) { + return repositories.stream() + .filter(MavenArtifactRepository.class::isInstance) + .map(MavenArtifactRepository.class::cast) + .map(repo -> MavenRepositoryImpl.builder() + .id(repo.getName()) + .uri(repo.getUrl().toString()) + .releases(String.valueOf(true)) + .snapshots(String.valueOf(true)) + .build()) + .collect(toList()); + } + + public static List pluginDescriptors(PluginManager pluginManager) { + if (pluginManager instanceof PluginManagerInternal) { + return pluginDescriptors((PluginManagerInternal) pluginManager); + } + return emptyList(); + } + + public static List pluginDescriptors(PluginManagerInternal pluginManager) { + return pluginManager.getPluginContainer() + .stream() + .map(plugin -> new GradlePluginDescriptorImpl(plugin.getClass().getName(), + pluginIdForClass(pluginManager, plugin.getClass()))) + .collect(toList()); + } + + private static String pluginIdForClass(PluginManagerInternal pluginManager, Class pluginClass) { + try { + Method findPluginIdForClass = PluginManagerInternal.class.getMethod("findPluginIdForClass", Class.class); + // noinspection unchecked + Optional maybePluginId = (Optional) findPluginIdForClass.invoke(pluginManager, + pluginClass); + return maybePluginId.map(PluginId::getId).orElse(null); + } + catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) { + // On old versions of gradle that don't have this method, returning null is + // fine + } + return null; + } + + private static final Map groupArtifactCache = new ConcurrentHashMap<>(); + + private static GroupArtifactImpl groupArtifact(Dependency dep) { + // noinspection ConstantConditions + return groupArtifactCache + .computeIfAbsent(new GroupArtifactImpl(dep.getGav().getGroupId(), dep.getGav().getArtifactId()), it -> it); + } + + private static GroupArtifactImpl groupArtifact(org.gradle.api.artifacts.ResolvedDependency dep) { + return groupArtifactCache.computeIfAbsent(new GroupArtifactImpl(dep.getModuleGroup(), dep.getModuleName()), + it -> it); + } + + private static final Map groupArtifactVersionCache = new ConcurrentHashMap<>(); + + private static GroupArtifactVersionImpl groupArtifactVersion(org.gradle.api.artifacts.ResolvedDependency dep) { + return groupArtifactVersionCache.computeIfAbsent(new GroupArtifactVersionImpl(dep.getModuleGroup(), + dep.getModuleName(), unspecifiedToNull(dep.getModuleVersion())), it -> it); + } + + private static GroupArtifactVersionImpl groupArtifactVersion(org.gradle.api.artifacts.Dependency dep) { + return groupArtifactVersionCache.computeIfAbsent( + new GroupArtifactVersionImpl(dep.getGroup(), dep.getName(), unspecifiedToNull(dep.getVersion())), + it -> it); + } + + private static final Map resolvedGroupArtifactVersionCache = new ConcurrentHashMap<>(); + + private static ResolvedGroupArtifactVersionImpl resolvedGroupArtifactVersion( + org.gradle.api.artifacts.ResolvedDependency dep) { + return resolvedGroupArtifactVersionCache.computeIfAbsent(new ResolvedGroupArtifactVersionImpl( + dep.getModuleName(), dep.getModuleGroup(), dep.getModuleVersion(), null), it -> it); + } + + /** + * Some Gradle dependency functions will have the String "unspecified" to indicate a + * missing value. Rewrite's dependency API represents these missing things as "null" + */ + private static String unspecifiedToNull(String maybeUnspecified) { + if ("unspecified".equals(maybeUnspecified)) { + return null; + } + return maybeUnspecified; + } + + static Map dependencyConfigurations( + ConfigurationContainer configurationContainer) { + Map results = new HashMap<>(); + List configurations = new ArrayList<>(configurationContainer); + for (Configuration conf : configurations) { + try { + List requested = conf.getAllDependencies() + .stream() + .map(dep -> dependency(dep, conf)) + .collect(Collectors.toList()); + + List resolved; + Map gaToRequested = requested.stream() + .collect(Collectors.toMap(GradleToolingApiProjectBuilder::groupArtifact, dep -> dep, (a, b) -> a)); + // Archives and default are redundant with other configurations + // Newer versions of gradle display warnings with long stack traces when + // attempting to resolve them + // Some Scala plugin we don't care about creates configurations that, for + // some unknown reason, are difficult to resolve + if (conf.isCanBeResolved() && !"archives".equals(conf.getName()) && !"default".equals(conf.getName()) + && !conf.getName().startsWith("incrementalScalaAnalysis")) { + ResolvedConfiguration resolvedConf = conf.getResolvedConfiguration(); + Map gaToResolved = resolvedConf + .getFirstLevelModuleDependencies() + .stream() + .collect(Collectors.toMap(dep -> GradleToolingApiProjectBuilder.groupArtifact(dep), dep -> dep, + (a, b) -> a)); + resolved = resolved(gaToRequested, gaToResolved); + } + else { + resolved = emptyList(); + } + // TODO: Doesn't look like 'transitive' is needed at all for Gradle + // tooling model side + List transitive = resolveTransitiveDependencies(resolved, new LinkedHashSet<>()); + GradleDependencyConfigurationImpl dc = new GradleDependencyConfigurationImpl(conf.getName(), + conf.getDescription(), conf.isTransitive(), conf.isCanBeConsumed(), conf.isCanBeResolved(), + emptyList(), requested, resolved); + results.put(conf.getName(), dc); + } + catch (Exception e) { + GradleDependencyConfigurationImpl dc = new GradleDependencyConfigurationImpl(conf.getName(), + conf.getDescription(), conf.isTransitive(), conf.isCanBeConsumed(), conf.isCanBeResolved(), + emptyList(), emptyList(), emptyList()); + results.put(conf.getName(), dc); + } + } + + // Record the relationships between dependency configurations + for (Configuration conf : configurations) { + if (conf.getExtendsFrom().isEmpty()) { + continue; + } + GradleDependencyConfigurationImpl dc = (GradleDependencyConfigurationImpl) results.get(conf.getName()); + if (dc != null) { + List extendsFrom = conf.getExtendsFrom() + .stream() + .map(it -> results.get(it.getName())) + .collect(Collectors.toList()); + dc.extendsFrom = extendsFrom; + } + } + return results; + } + + static List resolveTransitiveDependencies(List resolved, + Set alreadyResolved) { + for (ResolvedDependency dependency : resolved) { + if (alreadyResolved.add(dependency)) { + alreadyResolved.addAll(resolveTransitiveDependencies(dependency.getDependencies(), alreadyResolved)); + } + } + return new ArrayList<>(alreadyResolved); + } + + private static final Map requestedCache = new ConcurrentHashMap<>(); + + private static DependencyImpl dependency(org.gradle.api.artifacts.Dependency dep, Configuration configuration) { + GroupArtifactVersionImpl gav = groupArtifactVersion(dep); + return requestedCache.computeIfAbsent(gav, + it -> DependencyImpl.builder() + .gav(gav) + .type("jar") + .scope(configuration.getName()) + .exclusions(emptyList()) + .build()); + } + + private static List resolved(Map gaToRequested, + Map gaToResolved) { + Map resolvedCache = new HashMap<>(); + return gaToResolved.entrySet().stream().map(entry -> { + GroupArtifactImpl ga = entry.getKey(); + org.gradle.api.artifacts.ResolvedDependency resolved = entry.getValue(); + + // Gradle knows which repository it got a dependency from, but haven't been + // able to find where that info lives + ResolvedGroupArtifactVersionImpl resolvedGav = resolvedGroupArtifactVersion(resolved); + ResolvedDependencyImpl resolvedDependency = resolvedCache.get(resolvedGav); + if (resolvedDependency == null) { + resolvedDependency = ResolvedDependencyImpl.builder() + .gav(resolvedGav) + // There may not be a requested entry if a dependency substitution + // rule took effect + // the DependencyHandler has the substitution mapping buried inside + // it, but not exposed publicly + // Possible improvement to dig that out and use it + .requested((DependencyImpl) gaToRequested.getOrDefault(ga, dependency(resolved))) + .dependencies(resolved.getChildren() + .stream() + .map(child -> resolved(child, 1, resolvedCache)) + .collect(toList())) + .depth(0) + .build(); + resolvedCache.put(resolvedGav, resolvedDependency); + } + return resolvedDependency; + }).collect(Collectors.toList()); + } + + /** + * When there is a resolved dependency that cannot be matched up with a requested + * dependency, construct a requested dependency corresponding to the exact version + * which was resolved. This isn't strictly accurate, but there is no obvious way to + * access the resolution of transitive dependencies to figure out what versions are + * requested during the resolution process. + */ + private static DependencyImpl dependency(org.gradle.api.artifacts.ResolvedDependency dep) { + GroupArtifactVersionImpl gav = groupArtifactVersion(dep); + return requestedCache.computeIfAbsent(gav, + it -> DependencyImpl.builder() + .gav(gav) + .type("jar") + .scope(dep.getConfiguration()) + .exclusions(emptyList()) + .build()); + } + + private static ResolvedDependencyImpl resolved(org.gradle.api.artifacts.ResolvedDependency dep, int depth, + Map resolvedCache) { + ResolvedGroupArtifactVersionImpl resolvedGav = resolvedGroupArtifactVersion(dep); + ResolvedDependencyImpl resolvedDependency = resolvedCache.get(resolvedGav); + if (resolvedDependency == null) { + + List dependencies = new ArrayList<>(); + + resolvedDependency = ResolvedDependencyImpl.builder() + .gav(resolvedGav) + .requested(dependency(dep)) + .dependencies(dependencies) + .depth(depth) + .build(); + // we add a temporal resolved dependency in the cache to avoid stackoverflow + // with dependencies that have cycles + resolvedCache.put(resolvedGav, resolvedDependency); + dep.getChildren().forEach(child -> dependencies.add(resolved(child, depth + 1, resolvedCache))); + } + return resolvedDependency; + } + + @SuppressWarnings("unused") + public static void clearCaches() { + requestedCache.clear(); + groupArtifactCache.clear(); + groupArtifactVersionCache.clear(); + resolvedGroupArtifactVersionCache.clear(); + } + + public static GradleProjectData createProjectData(Project project) { + return GradleProjectDataImpl.from(project); + } + +} diff --git a/spring-rewrite-commons-gradle/rewrite-gradle-plugin/src/main/java/org/springframework/rewrite/gradle/plugin/model/GradleToolingApiSettingsBuilder.java b/spring-rewrite-commons-gradle/rewrite-gradle-plugin/src/main/java/org/springframework/rewrite/gradle/plugin/model/GradleToolingApiSettingsBuilder.java new file mode 100644 index 0000000..286737e --- /dev/null +++ b/spring-rewrite-commons-gradle/rewrite-gradle-plugin/src/main/java/org/springframework/rewrite/gradle/plugin/model/GradleToolingApiSettingsBuilder.java @@ -0,0 +1,110 @@ +/* + * Copyright 2021 - 2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.rewrite.gradle.plugin.model; + +import lombok.AllArgsConstructor; +import lombok.Value; +import org.gradle.api.initialization.Settings; +import org.gradle.api.internal.FeaturePreviews; +import org.gradle.initialization.DefaultSettings; +import org.gradle.internal.service.ServiceRegistry; +import org.gradle.internal.service.UnknownServiceException; +import org.gradle.util.GradleVersion; +import org.openrewrite.gradle.toolingapi.FeaturePreview; +import org.openrewrite.gradle.toolingapi.GradlePluginDescriptor; +import org.openrewrite.gradle.toolingapi.GradleSettings; +import org.openrewrite.gradle.toolingapi.MavenRepository; + +import java.io.Serializable; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.*; + +public class GradleToolingApiSettingsBuilder { + + @AllArgsConstructor + @Value + static class FeaturePreviewImpl implements FeaturePreview, Serializable { + + String name; + + boolean active; + + boolean enabled; + + } + + @AllArgsConstructor + @Value + static class GradleSettingsImpl implements GradleSettings, Serializable { + + List pluginRepositories; + + List plugins; + + Map featurePreviews; + + } + + public static GradleSettings gradleSettings(Settings settings) { + if (settings == null) { + return null; + } + Set pluginRepositories = new HashSet<>(); + pluginRepositories + .addAll(GradleToolingApiProjectBuilder.mapRepositories(settings.getPluginManagement().getRepositories())); + pluginRepositories + .addAll(GradleToolingApiProjectBuilder.mapRepositories(settings.getBuildscript().getRepositories())); + if (pluginRepositories.isEmpty()) { + pluginRepositories.add(GradleToolingApiProjectBuilder.GRADLE_PLUGIN_PORTAL); + } + + return new GradleSettingsImpl(new ArrayList<>(pluginRepositories), + GradleToolingApiProjectBuilder.pluginDescriptors(settings.getPluginManager()), + featurePreviews((DefaultSettings) settings)); + } + + private static Map featurePreviews(DefaultSettings settings) { + if (GradleVersion.current().compareTo(GradleVersion.version("4.6")) < 0) { + return Collections.emptyMap(); + } + + Map featurePreviews = new HashMap<>(); + FeaturePreviews gradleFeaturePreviews = getService(settings, FeaturePreviews.class); + if (gradleFeaturePreviews != null) { + FeaturePreviews.Feature[] gradleFeatures = FeaturePreviews.Feature.values(); + for (FeaturePreviews.Feature feature : gradleFeatures) { + // Unclear how enabled status can be determined in latest gradle APIs + featurePreviews.put(feature.name(), new FeaturePreviewImpl(feature.name(), feature.isActive(), false)); + } + } + return featurePreviews; + } + + private static T getService(DefaultSettings settings, + @SuppressWarnings("SameParameterValue") Class serviceType) { + try { + Method services = settings.getClass().getDeclaredMethod("getServices"); + services.setAccessible(true); + ServiceRegistry serviceRegistry = (ServiceRegistry) services.invoke(settings); + return serviceRegistry.get(serviceType); + } + catch (UnknownServiceException | NoSuchMethodException | IllegalAccessException | InvocationTargetException e) { + return null; + } + } + +} diff --git a/spring-rewrite-commons-gradle/rewrite-gradle-plugin/src/main/java/org/springframework/rewrite/gradle/plugin/model/JavaSourceSetDataImpl.java b/spring-rewrite-commons-gradle/rewrite-gradle-plugin/src/main/java/org/springframework/rewrite/gradle/plugin/model/JavaSourceSetDataImpl.java new file mode 100644 index 0000000..97bbc10 --- /dev/null +++ b/spring-rewrite-commons-gradle/rewrite-gradle-plugin/src/main/java/org/springframework/rewrite/gradle/plugin/model/JavaSourceSetDataImpl.java @@ -0,0 +1,46 @@ +/* + * Copyright 2021 - 2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.rewrite.gradle.plugin.model; + +import lombok.AllArgsConstructor; +import lombok.Value; +import org.springframework.rewrite.gradle.model.JavaSourceSetData; + +import java.io.File; +import java.io.Serializable; +import java.util.Collection; + +@AllArgsConstructor +@Value +class JavaSourceSetDataImpl implements JavaSourceSetData, Serializable { + + String name; + + Collection sources; + + Collection sourceDirectories; + + Collection java; + + Collection classesDirs; + + Collection compileClasspath; + + Collection implementationClasspath; + + JavaVersionDataImpl javaVersionData; + +} diff --git a/spring-rewrite-commons-gradle/rewrite-gradle-plugin/src/main/java/org/springframework/rewrite/gradle/plugin/model/JavaVersionDataImpl.java b/spring-rewrite-commons-gradle/rewrite-gradle-plugin/src/main/java/org/springframework/rewrite/gradle/plugin/model/JavaVersionDataImpl.java new file mode 100644 index 0000000..f642ea4 --- /dev/null +++ b/spring-rewrite-commons-gradle/rewrite-gradle-plugin/src/main/java/org/springframework/rewrite/gradle/plugin/model/JavaVersionDataImpl.java @@ -0,0 +1,36 @@ +/* + * Copyright 2021 - 2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.rewrite.gradle.plugin.model; + +import lombok.AllArgsConstructor; +import lombok.Value; +import org.springframework.rewrite.gradle.model.JavaVersionData; + +import java.io.Serializable; + +@AllArgsConstructor +@Value +class JavaVersionDataImpl implements JavaVersionData, Serializable { + + String createdBy; + + String vmVendor; + + String sourceCompatibility; + + String targetCompatibility; + +} \ No newline at end of file diff --git a/spring-rewrite-commons-gradle/rewrite-gradle-plugin/src/main/java/org/springframework/rewrite/gradle/plugin/model/KotlinSourceSetDataImpl.java b/spring-rewrite-commons-gradle/rewrite-gradle-plugin/src/main/java/org/springframework/rewrite/gradle/plugin/model/KotlinSourceSetDataImpl.java new file mode 100644 index 0000000..1ff42ae --- /dev/null +++ b/spring-rewrite-commons-gradle/rewrite-gradle-plugin/src/main/java/org/springframework/rewrite/gradle/plugin/model/KotlinSourceSetDataImpl.java @@ -0,0 +1,38 @@ +/* + * Copyright 2021 - 2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.rewrite.gradle.plugin.model; + +import lombok.AllArgsConstructor; +import lombok.Value; +import org.springframework.rewrite.gradle.model.KotlinSourceSetData; + +import java.io.File; +import java.io.Serializable; +import java.util.Collection; + +@AllArgsConstructor +@Value +class KotlinSourceSetDataImpl implements KotlinSourceSetData, Serializable { + + String name; + + Collection kotlin; + + Collection compileClasspath; + + Collection implementationClasspath; + +} \ No newline at end of file