Skip to content

Commit

Permalink
Add support for Android source sets (#327)
Browse files Browse the repository at this point in the history
* Add support for Android source sets

* optionalPlugins configuration naming correction

* Code consolidation

* Encapsulate android source set parsing in separate classes

* Add missing copyrights. Remove dead code

* Include test variants. Remove redundant AndroidProjectSpec

* Support for AGP versions 3.x

* Add AGP 3.* test

* Revert unrelated auto-formatting

* Correct regressions parsing multi-project repos

* Set progress bar callback when parsing Java/Kotlin source sets

* Fix JacksonXML version to 2.17.2

* Disable AGP 3 test on gradle versions less than 8

* Enable AGP 3 test on gradle versions less than 8

* Fix AGP 3 test for :plugin:testGradle4

* Add runtime dependencies collection.
Add test demonstrating that the same java source files are parsed twice

* Prevent repeated parsing of androud source files found in multiple variants

* Annotate static fields with @nullable

---------

Co-authored-by: Sam Snyder <[email protected]>
  • Loading branch information
bryceatmoderne and sambsnyd authored Sep 4, 2024
1 parent 3679f30 commit 8139f0e
Show file tree
Hide file tree
Showing 10 changed files with 1,207 additions and 302 deletions.
7 changes: 5 additions & 2 deletions plugin/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
import java.util.*

plugins {
kotlin("jvm") version("1.9.0")
id("org.jetbrains.kotlin.jvm") version "1.9.0"
id("com.gradle.plugin-publish") version "1.1.0"
id("com.github.hierynomus.license") version "0.16.1"
id("nebula.maven-apache-license")
Expand Down Expand Up @@ -46,6 +46,8 @@ repositories {
excludeVersionByRegex(".+", ".+", ".+-rc-?[0-9]*")
}
}
gradlePluginPortal()
google()
}

val latest = if (project.hasProperty("releasing")) {
Expand Down Expand Up @@ -116,9 +118,10 @@ dependencies {
"rewriteDependencies"("com.puppycrawl.tools:checkstyle:9.3") {
because("Latest version supporting gradle 4.x")
}
"rewriteDependencies"("com.fasterxml.jackson.module:jackson-module-kotlin:latest.release")
"rewriteDependencies"("com.fasterxml.jackson.module:jackson-module-kotlin:2.17.2")
"rewriteDependencies"("com.google.guava:guava:latest.release")
implementation(platform("org.openrewrite:rewrite-bom:$latest"))
compileOnly("com.android.tools.build:gradle:7.0.4")
compileOnly("org.jetbrains.kotlin:kotlin-gradle-plugin:latest.release")
compileOnly("com.google.guava:guava:latest.release")

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,15 @@

import org.gradle.api.Project;
import org.gradle.internal.service.ServiceRegistry;
import org.jspecify.annotations.Nullable;

import java.lang.reflect.InvocationTargetException;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URL;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.Set;
Expand All @@ -32,9 +34,11 @@
import java.util.stream.Collectors;

public class DelegatingProjectParser implements GradleProjectParser {
protected final GradleProjectParser gpp;
@Nullable
protected static List<URL> rewriteClasspath;
@Nullable
protected static RewriteClassLoader rewriteClassLoader;
protected final GradleProjectParser gpp;

public DelegatingProjectParser(Project project, RewriteExtension extension, Set<Path> classpath) {
try {
Expand All @@ -54,11 +58,16 @@ public DelegatingProjectParser(Project project, RewriteExtension extension, Set<
.getResource("/org/openrewrite/gradle/isolated/DefaultProjectParser.class")
.toString());
classpathUrls.add(currentJar);
if (rewriteClassLoader == null || !classpathUrls.equals(rewriteClasspath)) {

ClassLoader pluginClassLoader = getPluginClassLoader(project);

if (rewriteClassLoader == null ||
!classpathUrls.equals(rewriteClasspath) ||
rewriteClassLoader.getPluginClassLoader() != pluginClassLoader) {
if (rewriteClassLoader != null) {
rewriteClassLoader.close();
}
rewriteClassLoader = new RewriteClassLoader(classpathUrls);
rewriteClassLoader = new RewriteClassLoader(classpathUrls, pluginClassLoader);
rewriteClasspath = classpathUrls;
}

Expand Down Expand Up @@ -163,4 +172,30 @@ private <T> T unwrapInvocationException(Callable<T> supplier) {
throw new RuntimeException(e);
}
}

private ClassLoader getPluginClassLoader(Project project) {
ClassLoader pluginClassLoader = getAndroidPluginClassLoader(project);
if (pluginClassLoader == null) {
pluginClassLoader = getClass().getClassLoader();
}
return pluginClassLoader;
}

@Nullable
private ClassLoader getAndroidPluginClassLoader(Project project) {
List<String> pluginIds = Arrays.asList(
"com.android.application",
"com.android.library",
"com.android.feature",
"com.android.dynamic-feature",
"com.android.test");

for (String pluginId : pluginIds) {
if (project.getPlugins().hasPlugin(pluginId)) {
Object plugin = project.getPlugins().getPlugin(pluginId);
return plugin.getClass().getClassLoader();
}
}
return null;
}
}
51 changes: 36 additions & 15 deletions plugin/src/main/java/org/openrewrite/gradle/RewriteClassLoader.java
Original file line number Diff line number Diff line change
Expand Up @@ -29,34 +29,47 @@
*/
public class RewriteClassLoader extends URLClassLoader {

private static final List<String> loadFromParent = Arrays.asList(
"org.openrewrite.gradle.GradleProjectParser",
"org.openrewrite.gradle.DefaultRewriteExtension",
"org.openrewrite.gradle.RewriteExtension",
"org.slf4j",
"org.gradle",
"groovy",
"org.codehaus.groovy"
);
private static final List<String> PARENT_LOADED_PACKAGES = Arrays.asList(
"org.openrewrite.gradle.GradleProjectParser",
"org.openrewrite.gradle.DefaultRewriteExtension",
"org.openrewrite.gradle.RewriteExtension",
"org.slf4j",
"org.gradle",
"groovy",
"org.codehaus.groovy");
private static final List<String> PLUGIN_LOADED_PACKAGES = Arrays.asList("com.android");
private final ClassLoader pluginClassLoader;

public RewriteClassLoader(Collection<URL> artifacts) {
this(artifacts, RewriteClassLoader.class.getClassLoader());
}

public RewriteClassLoader(Collection<URL> artifacts, ClassLoader pluginClassLoader) {
super(artifacts.toArray(new URL[0]), RewriteClassLoader.class.getClassLoader());
this.pluginClassLoader = pluginClassLoader;
setDefaultAssertionStatus(true);
}

public ClassLoader getPluginClassLoader() {
return pluginClassLoader;
}

/**
* Load the named class. We want classes that extend <code>org.openrewrite.Recipe</code> to be loaded
* by this ClassLoader <strong>only</strong>. But we want classes required to run recipes to continue
* to be loaded by their parent ClassLoader to avoid <code>ClassCastException</code>s.
* to be loaded by their parent ClassLoader to avoid <code>ClassCastException</code>s. In the case
* of Android Gradle plugin classes, we use the ClassLoader of the plugin.
*/
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
Class<?> foundClass = findLoadedClass(name);
if (foundClass == null) {
try {
if (!shouldBeParentLoaded(name)) {
foundClass = findClass(name);
} else {
if (shouldBeParentLoaded(name)) {
foundClass = super.loadClass(name, resolve);
} else if (shouldBePluginLoaded(name)) {
foundClass = Class.forName(name, resolve, pluginClassLoader);
} else {
foundClass = findClass(name);
}
} catch (ClassNotFoundException e) {
foundClass = super.loadClass(name, resolve);
Expand All @@ -69,8 +82,16 @@ protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundE
}

protected boolean shouldBeParentLoaded(String name) {
for (String s : loadFromParent) {
if (name.startsWith(s)) {
return shouldBeLoaded(name, PARENT_LOADED_PACKAGES);
}

protected boolean shouldBePluginLoaded(String name) {
return shouldBeLoaded(name, PLUGIN_LOADED_PACKAGES);
}

private boolean shouldBeLoaded(String name, List<String> packagesToLoad) {
for (String pkg : packagesToLoad) {
if (name.startsWith(pkg)) {
return true;
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
/*
* Copyright 2024 the original author or authors.
* <p>
* 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
* <p>
* https://www.apache.org/licenses/LICENSE-2.0
* <p>
* 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.openrewrite.gradle.isolated;

import com.android.build.gradle.BaseExtension;
import org.gradle.api.JavaVersion;

import java.lang.reflect.Method;
import java.nio.charset.Charset;

/*
* AGP versions less than 4 define CompileOptions in com.android.build.gradle.internal.CompileOptions where as
* versions greater than 4 define it in com.android.build.api.dsl.CompileOptions. This class encapsulates fetching
* CompileOptions using either type.
*/
class AndroidProjectCompileOptions {
private final Charset encoding;
private final String sourceCompatibility;
private final String targetCompatibility;

AndroidProjectCompileOptions(Charset encoding, String sourceCompatibility, String targetCompatibility) {
this.encoding = encoding;
this.sourceCompatibility = sourceCompatibility;
this.targetCompatibility = targetCompatibility;
}

static AndroidProjectCompileOptions fromBaseExtension(BaseExtension baseExtension) throws ReflectiveOperationException {
Object compileOptions = callMethod(baseExtension, "getCompileOptions");
String fileEncoding = callMethod(compileOptions, "getEncoding");
JavaVersion sourceCompatibilityVersion = callMethod(compileOptions, "getSourceCompatibility");
JavaVersion targetCompatibilityVersion = callMethod(compileOptions, "getTargetCompatibility");
return new AndroidProjectCompileOptions(
Charset.forName(fileEncoding),
sourceCompatibilityVersion.toString(),
targetCompatibilityVersion.toString());
}

private static <T> T callMethod(Object obj, String methodName) throws ReflectiveOperationException {
Method method = obj.getClass().getMethod(methodName);
return (T) method.invoke(obj);
}

Charset getEncoding() {
return encoding;
}

String getSourceCompatibility() {
return sourceCompatibility;
}

String getTargetCompatibility() {
return targetCompatibility;
}
}
Loading

0 comments on commit 8139f0e

Please sign in to comment.