Gradle Project Parser - initial implementation (#7)

This commit is contained in:
Alex Boyko
2023-12-21 10:27:15 -05:00
committed by GitHub
parent 8fc77a20bc
commit 866f810f77
25 changed files with 2468 additions and 0 deletions

View File

@@ -15,6 +15,7 @@
<module>spring-rewrite-commons-launcher</module>
<module>spring-rewrite-commons-docs</module>
<module>spring-rewrite-commons-examples</module>
<module>spring-rewrite-commons-gradle</module>
</modules>
<organization>
@@ -30,6 +31,8 @@
<!-- prod dependencies -->
<spring-boot.version>3.1.3</spring-boot.version>
<rewrite.version>8.5.1</rewrite.version>
<rewrite-kotlin.version>1.8.2</rewrite-kotlin.version>
<rewrite-polyglot.version>1.8.9</rewrite-polyglot.version>
<rewrite-maven-plugin.version>5.3.2</rewrite-maven-plugin.version>
<jaxb-api.version>2.3.1</jaxb-api.version>

View File

@@ -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

View File

@@ -0,0 +1,35 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.rewrite</groupId>
<artifactId>spring-rewrite-commons</artifactId>
<version>0.1.0-SNAPSHOT</version>
</parent>
<artifactId>spring-rewrite-commons-gradle</artifactId>
<packaging>pom</packaging>
<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<gradle.version>8.4</gradle.version>
</properties>
<modules>
<module>rewrite-gradle-model</module>
<module>rewrite-gradle-plugin</module>
<module>rewrite-gradle-parser</module>
</modules>
<repositories>
<repository>
<id>gradle</id>
<url>https://repo.gradle.org/gradle/libs-releases</url>
</repository>
</repositories>
</project>

View File

@@ -0,0 +1,48 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.rewrite</groupId>
<artifactId>spring-rewrite-commons-gradle</artifactId>
<version>0.1.0-SNAPSHOT</version>
</parent>
<artifactId>rewrite-gradle-model</artifactId>
<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<dependency>
<groupId>org.openrewrite.gradle.tooling</groupId>
<artifactId>model</artifactId>
<version>1.1.2</version>
</dependency>
<dependency>
<groupId>org.gradle</groupId>
<artifactId>gradle-tooling-api</artifactId>
<version>${gradle.version}</version>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-engine</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>

View File

@@ -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<GradleProjectData> getSubprojects();
File getProjectDir();
File getBuildDir();
File getBuildscriptFile();
Map<String, ?> getProperties();
List<JavaSourceSetData> getJavaSourceSets();
boolean isMultiPlatformKotlinProject();
List<KotlinSourceSetData> getKotlinSourceSets();
Collection<File> getBuildscriptClasspath();
Collection<File> getSettingsClasspath();
}

View File

@@ -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<File> getSources();
Collection<File> getSourceDirectories();
Collection<File> getJava();
Collection<File> getClassesDirs();
Collection<File> getCompileClasspath();
Collection<File> getImplementationClasspath();
JavaVersionData getJavaVersionData();
}

View File

@@ -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();
}

View File

@@ -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<File> getKotlin();
Collection<File> getCompileClasspath();
Collection<File> getImplementationClasspath();
}

View File

@@ -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> T forProjectDirectory(Class<T> 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<String> 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<T> 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);
}
}

View File

@@ -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
}

View File

@@ -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/");
}
}

View File

@@ -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")
}

View File

@@ -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"

View File

@@ -0,0 +1,124 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.rewrite</groupId>
<artifactId>spring-rewrite-commons-gradle</artifactId>
<version>0.1.0-SNAPSHOT</version>
</parent>
<artifactId>rewrite-gradle-parser</artifactId>
<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.rewrite</groupId>
<artifactId>rewrite-gradle-model</artifactId>
<version>0.1.0-SNAPSHOT</version>
</dependency>
<!-- https://mvnrepository.com/artifact/dev.gradleplugins/gradle-api -->
<dependency>
<groupId>dev.gradleplugins</groupId>
<artifactId>gradle-api</artifactId>
<version>${gradle.version}</version>
</dependency>
<dependency>
<groupId>org.openrewrite</groupId>
<artifactId>rewrite-core</artifactId>
<version>${rewrite.version}</version>
</dependency>
<dependency>
<groupId>org.openrewrite</groupId>
<artifactId>rewrite-gradle</artifactId>
<version>${rewrite.version}</version>
</dependency>
<dependency>
<groupId>org.openrewrite</groupId>
<artifactId>rewrite-groovy</artifactId>
<version>${rewrite.version}</version>
</dependency>
<dependency>
<groupId>org.openrewrite</groupId>
<artifactId>rewrite-hcl</artifactId>
<version>${rewrite.version}</version>
</dependency>
<dependency>
<groupId>org.openrewrite</groupId>
<artifactId>rewrite-java</artifactId>
<version>${rewrite.version}</version>
</dependency>
<dependency>
<groupId>org.openrewrite</groupId>
<artifactId>rewrite-java-8</artifactId>
<version>${rewrite.version}</version>
</dependency>
<dependency>
<groupId>org.openrewrite</groupId>
<artifactId>rewrite-java-11</artifactId>
<version>${rewrite.version}</version>
</dependency>
<dependency>
<groupId>org.openrewrite</groupId>
<artifactId>rewrite-java-17</artifactId>
<version>${rewrite.version}</version>
</dependency>
<dependency>
<groupId>org.openrewrite</groupId>
<artifactId>rewrite-json</artifactId>
<version>${rewrite.version}</version>
</dependency>
<dependency>
<groupId>org.openrewrite</groupId>
<artifactId>rewrite-kotlin</artifactId>
<version>${rewrite-kotlin.version}</version>
</dependency>
<dependency>
<groupId>org.openrewrite</groupId>
<artifactId>rewrite-properties</artifactId>
<version>${rewrite.version}</version>
</dependency>
<dependency>
<groupId>org.openrewrite</groupId>
<artifactId>rewrite-protobuf</artifactId>
<version>${rewrite.version}</version>
</dependency>
<dependency>
<groupId>org.openrewrite</groupId>
<artifactId>rewrite-xml</artifactId>
<version>${rewrite.version}</version>
</dependency>
<dependency>
<groupId>org.openrewrite</groupId>
<artifactId>rewrite-yaml</artifactId>
<version>${rewrite.version}</version>
</dependency>
<dependency>
<groupId>org.openrewrite</groupId>
<artifactId>rewrite-polyglot</artifactId>
<version>${rewrite-polyglot.version}</version>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-engine</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>

View File

@@ -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<String> exclusions, boolean logCompilationWarningsAndErrors, List<String> plainTextMasks,
int sizeThresholdMb, List<NamedStyles> styles) {
}

View File

@@ -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<Marker> 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<Path> listSources() {
// Use a sorted collection so that gradle input detection isn't thrown off by
// ordering
Set<Path> 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<SourceFile> parse(ExecutionContext ctx) {
Stream<SourceFile> builder = Stream.of();
Set<Path> 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<SourceFile> parse(GradleProjectData subproject, Set<Path> 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<PathMatcher> 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<NamedStyles> styles = options.styles();
logger.info("Using active styles {}", styles.stream().map(NamedStyles::getName).collect(toList()));
List<JavaSourceSetData> 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<Marker> 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<String> sourceDirs = new HashSet<>();
for (JavaSourceSetData sourceSet : sourceSets) {
Stream<SourceFile> 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<Path> 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<Path> javaPaths = unparsedSources.stream()
.filter(it -> it.toString().endsWith(".java") && !alreadyParsed.contains(it))
.collect(toList());
Collection<File> 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<Path> 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<SourceFile> cus = Stream
.of((Supplier<JavaParser>) () -> 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<Path> kotlinPaths = unparsedSources.stream()
.filter(it -> !it.toString().startsWith(excludedProtosPath))
.filter(it -> it.toString().endsWith(".kt"))
.collect(toList());
if (!kotlinPaths.isEmpty()) {
alreadyParsed.addAll(kotlinPaths);
Stream<SourceFile> cus = Stream
.of((Supplier<KotlinParser>) () -> 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<Path> 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<Path> dependenciesWithBuildDirs = Stream
.concat(dependencyPaths.stream(), sourceSet.getClassesDirs().stream().map(File::toPath))
.collect(toList());
alreadyParsed.addAll(groovyPaths);
Stream<SourceFile> cus = Stream
.of((Supplier<GroovyParser>) () -> 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<Path> 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<Path> settingsClasspath = project.getSettingsClasspath().stream().map(File::toPath).collect(toList());
List<Path> 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<PathMatcher> exclusions, Set<Path> alreadyParsed,
ExecutionContext ctx) {
Stream<SourceFile> 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<Path> alreadyParsed,
ExecutionContext ctx, List<Marker> projectProvenance, Stream<SourceFile> sourceFiles) {
// Collect any additional yaml/properties/xml files that are NOT already in a
// source set.
OmniParser omniParser = omniParser(alreadyParsed);
List<Path> accepted = omniParser.acceptedPaths(baseDir, subproject.getProjectDir().toPath());
return SourceFileStream.build("", s -> {
}).concat(omniParser.parse(accepted, baseDir, ctx), accepted.size());
}
private OmniParser omniParser(Set<Path> 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<String> 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<PathMatcher> pathMatchers(Path basePath, Collection<String> pathExpressions) {
return pathExpressions.stream()
.map(o -> basePath.getFileSystem().getPathMatcher("glob:" + o))
.collect(toList());
}
private SourceFileStream parseMultiplatformKotlinProject(GradleProjectData subproject,
Collection<PathMatcher> exclusions, Set<Path> alreadyParsed, ExecutionContext ctx) {
SourceFileStream sourceFileStream = SourceFileStream.build(subproject.getPath(), s -> {
});
for (KotlinSourceSetData sourceSet : project.getKotlinSourceSets()) {
List<Path> 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<Path> 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<SourceFile> 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<PathMatcher> 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 <T extends SourceFile> UnaryOperator<T> addProvenance(List<Marker> projectProvenance) {
return s -> {
Markers m = s.getMarkers();
for (Marker marker : projectProvenance) {
m = m.addIfAbsent(marker);
}
return s.withMarkers(m);
};
}
private <T extends SourceFile> UnaryOperator<T> addProvenance(Marker sourceSet) {
return s -> {
Markers m = s.getMarkers();
m = m.addIfAbsent(sourceSet);
return s.withMarkers(m);
};
}
public Path getBaseDir() {
return baseDir;
}
}

View File

@@ -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<SourceFile> sources = new ProjectParser(gp, OPTIONS, log)
.parse(new InMemoryExecutionContext(t -> Assertions.fail("Parser Error", t)))
.toList();
assertThat(sources.size()).isEqualTo(114);
}
}

View File

@@ -0,0 +1,66 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.rewrite</groupId>
<artifactId>spring-rewrite-commons-gradle</artifactId>
<version>0.1.0-SNAPSHOT</version>
</parent>
<artifactId>rewrite-gradle-plugin</artifactId>
<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<lombok.version>1.18.30</lombok.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.rewrite</groupId>
<artifactId>rewrite-gradle-model</artifactId>
<version>0.1.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok.version}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>dev.gradleplugins</groupId>
<artifactId>gradle-api</artifactId>
<version>${gradle.version}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>javax.inject</groupId>
<artifactId>javax.inject</artifactId>
<version>1</version>
<scope>compile</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.11.0</version>
<configuration>
<annotationProcessorPaths>
<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok.version}</version>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>
</plugins>
</build>
</project>

View File

@@ -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<Project> {
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);
}
}
}

View File

@@ -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<GradlePluginDescriptor> plugins;
List<MavenRepository> mavenRepositories;
List<MavenRepository> mavenPluginRepositories;
Map<String, GradleDependencyConfiguration> nameToConfiguration;
GradleSettings gradleSettings;
String gradleVersion;
boolean rootProject;
File rootProjectDir;
Collection<GradleProjectData> subprojects;
File projectDir;
File buildDir;
File buildscriptFile;
Map<String, ?> properties;
List<JavaSourceSetData> javaSourceSets;
boolean multiPlatformKotlinProject;
List<KotlinSourceSetData> kotlinSourceSets;
Collection<File> buildscriptClasspath;
Collection<File> 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<GradleProjectData> subprojects(Collection<Project> subprojects) {
List<GradleProjectData> sub = new ArrayList(subprojects.size());
for (Project s : subprojects) {
sub.add(from(s));
}
return sub;
}
private static Map<String, ?> properties(Map<String, ?> 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<JavaSourceSetData> javaSourceSets(Project project) {
JavaPluginConvention javaConvention = (JavaPluginConvention) project.getConvention()
.findPlugin(JavaPluginConvention.class);
if (javaConvention == null) {
return Collections.emptyList();
}
else {
List<JavaSourceSetData> 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<File> 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<KotlinSourceSetData> 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<String> sourceSetNames;
try {
sourceSetNames = (SortedSet<String>) sourceSets.getClass().getMethod("getNames").invoke(sourceSets);
}
catch (Exception e) {
return Collections.emptyList();
}
List<KotlinSourceSetData> 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<File> 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<File> 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();
}
}

View File

@@ -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<GroupArtifact> 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<ResolvedDependency> dependencies;
int depth;
}
@AllArgsConstructor
@Getter
static class GradleDependencyConfigurationImpl implements GradleDependencyConfiguration, Serializable {
String name;
String description;
boolean transitive;
boolean canBeConsumed;
boolean canBeResolved;
List<GradleDependencyConfiguration> extendsFrom;
List<Dependency> requested;
List<ResolvedDependency> resolved;
}
@AllArgsConstructor
@Getter
static class GradleProjectImpl implements GradleProject, Serializable {
String name;
String path;
List<GradlePluginDescriptor> plugins;
List<MavenRepository> mavenRepositories;
List<MavenRepository> mavenPluginRepositories;
Map<String, GradleDependencyConfiguration> 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<MavenRepository> pluginMavenRepos(Project project) {
Set<MavenRepository> 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<MavenRepository> mapRepositories(List<ArtifactRepository> 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<GradlePluginDescriptor> pluginDescriptors(PluginManager pluginManager) {
if (pluginManager instanceof PluginManagerInternal) {
return pluginDescriptors((PluginManagerInternal) pluginManager);
}
return emptyList();
}
public static List<GradlePluginDescriptor> 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<PluginId> maybePluginId = (Optional<PluginId>) 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<GroupArtifactImpl, GroupArtifactImpl> 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<GroupArtifactVersionImpl, GroupArtifactVersionImpl> 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<ResolvedGroupArtifactVersionImpl, ResolvedGroupArtifactVersionImpl> 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<String, GradleDependencyConfiguration> dependencyConfigurations(
ConfigurationContainer configurationContainer) {
Map<String, GradleDependencyConfiguration> results = new HashMap<>();
List<Configuration> configurations = new ArrayList<>(configurationContainer);
for (Configuration conf : configurations) {
try {
List<Dependency> requested = conf.getAllDependencies()
.stream()
.map(dep -> dependency(dep, conf))
.collect(Collectors.toList());
List<ResolvedDependency> resolved;
Map<GroupArtifactImpl, Dependency> 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<GroupArtifactImpl, org.gradle.api.artifacts.ResolvedDependency> 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<ResolvedDependency> 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<GradleDependencyConfiguration> extendsFrom = conf.getExtendsFrom()
.stream()
.map(it -> results.get(it.getName()))
.collect(Collectors.toList());
dc.extendsFrom = extendsFrom;
}
}
return results;
}
static List<ResolvedDependency> resolveTransitiveDependencies(List<ResolvedDependency> resolved,
Set<ResolvedDependency> alreadyResolved) {
for (ResolvedDependency dependency : resolved) {
if (alreadyResolved.add(dependency)) {
alreadyResolved.addAll(resolveTransitiveDependencies(dependency.getDependencies(), alreadyResolved));
}
}
return new ArrayList<>(alreadyResolved);
}
private static final Map<GroupArtifactVersionImpl, DependencyImpl> 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<ResolvedDependency> resolved(Map<GroupArtifactImpl, Dependency> gaToRequested,
Map<GroupArtifactImpl, org.gradle.api.artifacts.ResolvedDependency> gaToResolved) {
Map<ResolvedGroupArtifactVersionImpl, ResolvedDependencyImpl> 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<ResolvedGroupArtifactVersionImpl, ResolvedDependencyImpl> resolvedCache) {
ResolvedGroupArtifactVersionImpl resolvedGav = resolvedGroupArtifactVersion(dep);
ResolvedDependencyImpl resolvedDependency = resolvedCache.get(resolvedGav);
if (resolvedDependency == null) {
List<ResolvedDependency> 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);
}
}

View File

@@ -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<MavenRepository> pluginRepositories;
List<GradlePluginDescriptor> plugins;
Map<String, FeaturePreview> featurePreviews;
}
public static GradleSettings gradleSettings(Settings settings) {
if (settings == null) {
return null;
}
Set<MavenRepository> 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<String, FeaturePreview> featurePreviews(DefaultSettings settings) {
if (GradleVersion.current().compareTo(GradleVersion.version("4.6")) < 0) {
return Collections.emptyMap();
}
Map<String, FeaturePreview> 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> T getService(DefaultSettings settings,
@SuppressWarnings("SameParameterValue") Class<T> 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;
}
}
}

View File

@@ -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<File> sources;
Collection<File> sourceDirectories;
Collection<File> java;
Collection<File> classesDirs;
Collection<File> compileClasspath;
Collection<File> implementationClasspath;
JavaVersionDataImpl javaVersionData;
}

View File

@@ -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;
}

View File

@@ -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<File> kotlin;
Collection<File> compileClasspath;
Collection<File> implementationClasspath;
}