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