diff --git a/WordPress/build.gradle b/WordPress/build.gradle index 1a7c673ac696..e183e115dff1 100644 --- a/WordPress/build.gradle +++ b/WordPress/build.gradle @@ -12,6 +12,7 @@ plugins { id "com.google.gms.google-services" id "com.google.dagger.hilt.android" id "org.jetbrains.kotlinx.kover" + id "com.google.devtools.ksp" } sentry { @@ -345,7 +346,7 @@ dependencies { implementation 'androidx.webkit:webkit:1.10.0' implementation "androidx.navigation:navigation-compose:$androidxComposeNavigationVersion" compileOnly project(path: ':libs:annotations') - kapt project(':libs:processors') + ksp project(':libs:processors') implementation (project(path:':libs:networking')) { exclude group: "com.android.volley" exclude group: 'org.wordpress', module: 'utils' diff --git a/build.gradle b/build.gradle index eba5484f157b..40434f7d0e0f 100644 --- a/build.gradle +++ b/build.gradle @@ -9,6 +9,7 @@ plugins { id "com.android.library" apply false id 'com.google.gms.google-services' apply false id "org.jetbrains.kotlin.plugin.parcelize" apply false + id "com.google.devtools.ksp" apply false } ext { @@ -43,7 +44,7 @@ ext { androidxArchCoreVersion = '2.2.0' androidxCameraVersion = '1.2.3' androidxComposeBomVersion = '2023.10.00' - androidxComposeCompilerVersion = '1.5.3' + androidxComposeCompilerVersion = '1.5.9' androidxComposeNavigationVersion = '2.7.6' androidxCardviewVersion = '1.0.0' androidxConstraintlayoutVersion = '2.1.4' diff --git a/libs/processors/build.gradle b/libs/processors/build.gradle index 77eb4965cfe2..6baeb1bb411d 100644 --- a/libs/processors/build.gradle +++ b/libs/processors/build.gradle @@ -1,6 +1,5 @@ plugins { id "org.jetbrains.kotlin.jvm" - id "org.jetbrains.kotlin.kapt" id "org.jetbrains.kotlinx.kover" } @@ -10,11 +9,13 @@ targetCompatibility = JavaVersion.VERSION_1_8 dependencies { implementation project(":libs:annotations") - implementation "com.google.auto.service:auto-service:$googleAutoServiceVersion" - kapt "com.google.auto.service:auto-service:$googleAutoServiceVersion" implementation "com.squareup:kotlinpoet:$squareupKotlinPoetVersion" + implementation "com.squareup:kotlinpoet-ksp:$squareupKotlinPoetVersion" + implementation "com.google.devtools.ksp:symbol-processing-api:$gradle.ext.kspVersion" - testImplementation "com.github.tschuchortdev:kotlin-compile-testing:1.5.0" + def kctVersion = "1.5.0" + testImplementation "com.github.tschuchortdev:kotlin-compile-testing:$kctVersion" + testImplementation "com.github.tschuchortdev:kotlin-compile-testing-ksp:$kctVersion" testImplementation "junit:junit:$junitVersion" testImplementation "org.assertj:assertj-core:$assertjVersion" testImplementation "org.jetbrains.kotlin:kotlin-reflect:$gradle.ext.kotlinVersion" diff --git a/libs/processors/src/main/java/org/wordpress/android/processor/RemoteConfigProcessor.kt b/libs/processors/src/main/java/org/wordpress/android/processor/RemoteConfigProcessor.kt index a8217dd7084f..2c5c57acd01b 100644 --- a/libs/processors/src/main/java/org/wordpress/android/processor/RemoteConfigProcessor.kt +++ b/libs/processors/src/main/java/org/wordpress/android/processor/RemoteConfigProcessor.kt @@ -1,128 +1,131 @@ +@file:OptIn(KspExperimental::class) + + package org.wordpress.android.processor -import com.google.auto.service.AutoService -import com.squareup.kotlinpoet.DelicateKotlinPoetApi -import com.squareup.kotlinpoet.TypeName -import com.squareup.kotlinpoet.asTypeName +import com.google.devtools.ksp.KspExperimental +import com.google.devtools.ksp.containingFile +import com.google.devtools.ksp.getAnnotationsByType +import com.google.devtools.ksp.processing.CodeGenerator +import com.google.devtools.ksp.processing.Resolver +import com.google.devtools.ksp.processing.SymbolProcessor +import com.google.devtools.ksp.symbol.KSAnnotated +import com.google.devtools.ksp.symbol.KSClassDeclaration +import com.squareup.kotlinpoet.ksp.toTypeName +import com.squareup.kotlinpoet.ksp.writeTo import org.wordpress.android.annotation.Experiment import org.wordpress.android.annotation.Feature -import org.wordpress.android.annotation.FeatureInDevelopment import org.wordpress.android.annotation.RemoteFieldDefaultGenerater -import java.io.File -import javax.annotation.processing.AbstractProcessor -import javax.annotation.processing.Processor -import javax.annotation.processing.RoundEnvironment -import javax.annotation.processing.SupportedAnnotationTypes -import javax.annotation.processing.SupportedSourceVersion -import javax.lang.model.SourceVersion -import javax.lang.model.element.TypeElement -import javax.tools.Diagnostic.Kind - -@AutoService(Processor::class) // For registering the service -@SupportedSourceVersion(SourceVersion.RELEASE_8) // to support Java 8 -@SupportedAnnotationTypes( - "org.wordpress.android.annotation.Experiment", - "org.wordpress.android.annotation.Feature", - "org.wordpress.android.annotation.FeatureInDevelopment", - "org.wordpress.android.annotation.RemoteFieldDefaultGenerater" -) -class RemoteConfigProcessor : AbstractProcessor() { - @OptIn(DelicateKotlinPoetApi::class) - @Suppress("DEPRECATION") - override fun process(p0: MutableSet?, roundEnvironment: RoundEnvironment?): Boolean { - val experiments = roundEnvironment?.getElementsAnnotatedWith(Experiment::class.java)?.map { element -> - val annotation = element.getAnnotation(Experiment::class.java) - annotation.remoteField to annotation.defaultVariant - } ?: listOf() - val remoteFeatureNames = mutableListOf() - val features = roundEnvironment?.getElementsAnnotatedWith(Feature::class.java)?.map { element -> - val annotation = element.getAnnotation(Feature::class.java) - remoteFeatureNames.add(element.asType().asTypeName()) - annotation.remoteField to annotation.defaultValue.toString() - } ?: listOf() - val remoteFields = roundEnvironment?.getElementsAnnotatedWith(RemoteFieldDefaultGenerater::class.java) - ?.map { element -> - val annotation = element.getAnnotation(RemoteFieldDefaultGenerater::class.java) - annotation.remoteField to annotation.defaultValue - } ?: listOf() - val featuresInDevelopment = roundEnvironment?.getElementsAnnotatedWith(FeatureInDevelopment::class.java) - ?.map { element -> - element.asType().toString() - } ?: listOf() - return if (experiments.isNotEmpty() || features.isNotEmpty()) { - generateRemoteFieldConfigDefaults(remoteFields.toMap()) - generateRemoteFeatureConfigDefaults((experiments + features).toMap()) - generateRemoteFeatureConfigCheck(remoteFeatureNames) - generateFeaturesInDevelopment(featuresInDevelopment) - true - } else { - false + +@OptIn(KspExperimental::class) +class RemoteConfigProcessor( + private val codeGenerator: CodeGenerator, +) : SymbolProcessor { + /** + * In the case of this processor, we only one need round. Generated files do not depend on each other + * or any other processor. + * + * See: https://github.com/google/ksp/issues/797#issuecomment-1041127747 + * Also: https://github.com/google/ksp/blob/a0cd7774a7f65cec45a50ecc8960ef5e4d47fc21/examples/playground/test-processor/src/main/kotlin/TestProcessor.kt#L20 + */ + private var invoked = false + + override fun process(resolver: Resolver): List { + if (invoked) { + return emptyList() } + + val remoteFeatures = resolver.getSymbolsWithAnnotation("org.wordpress.android.annotation.Feature") + .toList() + + generateRemoteFeatureConfigDefaults(resolver, remoteFeatures) + generateRemoteFieldsConfigDefaults(resolver) + generateFeaturesInDevelopment(resolver) + generateRemoteFeatureConfigCheck(remoteFeatures) + + invoked = true + return emptyList() } - @Suppress("TooGenericExceptionCaught", "SwallowedException") - private fun generateRemoteFeatureConfigDefaults( - remoteConfigDefaults: Map - ) { - try { - val fileContent = RemoteFeatureConfigDefaultsBuilder(remoteConfigDefaults).getContent() - - val kaptKotlinGeneratedDir = processingEnv.options[KAPT_KOTLIN_GENERATED_OPTION_NAME] - fileContent.writeTo(File(kaptKotlinGeneratedDir)) - } catch (e: Exception) { - processingEnv.messager.printMessage(Kind.ERROR, "Failed to generate remote feature config defaults") + private fun generateRemoteFeatureConfigDefaults(resolver: Resolver, remoteFeatures: List) { + val experiments = resolver.getSymbolsWithAnnotation("org.wordpress.android.annotation.Experiment") + .toList() + + val defaults = (remoteFeatures + experiments) + .map { element: KSAnnotated -> + val featuresDefaults = element.getAnnotationsByType(Feature::class) + .toList().associate { annotation -> + annotation.remoteField to annotation.defaultValue.toString() + } + val experimentsDefaults = element.getAnnotationsByType(Experiment::class).toList() + .toList().associate { annotation -> + annotation.remoteField to annotation.defaultVariant + } + featuresDefaults + experimentsDefaults + }.flatMap { it.toList() } + .toMap() + + if (defaults.isNotEmpty()) { + RemoteFeatureConfigDefaultsBuilder(defaults).getContent() + .writeTo( + codeGenerator, + aggregating = true, + originatingKSFiles = remoteFeatures.map { it.containingFile!! } + ) } } - @Suppress("TooGenericExceptionCaught", "SwallowedException") - private fun generateRemoteFieldConfigDefaults( - remoteConfigDefaults: Map - ) { - try { - val fileContent = RemoteFieldConfigDefaultsBuilder(remoteConfigDefaults).getContent() - - val kaptKotlinGeneratedDir = processingEnv.options[KAPT_KOTLIN_GENERATED_OPTION_NAME] - fileContent.writeTo(File(kaptKotlinGeneratedDir)) - } catch (e: Exception) { - processingEnv.messager.printMessage(Kind.ERROR, "Failed to generate remote feature config defaults") + private fun generateRemoteFieldsConfigDefaults(resolver: Resolver) { + val remoteFields = + resolver.getSymbolsWithAnnotation("org.wordpress.android.annotation.RemoteFieldDefaultGenerater") + .toList() + val remoteFieldDefaults = remoteFields + .associate { element: KSAnnotated -> + element.getAnnotationsByType(RemoteFieldDefaultGenerater::class) + .toList() + .first() + .let { annotation -> + annotation.remoteField to annotation.defaultValue + } + } + + if(remoteFieldDefaults.isNotEmpty()) { + RemoteFieldConfigDefaultsBuilder(remoteFieldDefaults).getContent() + .writeTo( + codeGenerator, + aggregating = true, + originatingKSFiles = remoteFields.map { it.containingFile!! } + ) } } - @Suppress("TooGenericExceptionCaught") - private fun generateRemoteFeatureConfigCheck( - remoteFeatureNames: List - ) { - try { - val fileContent = RemoteFeatureConfigCheckBuilder(remoteFeatureNames).getContent() - - val kaptKotlinGeneratedDir = processingEnv.options[KAPT_KOTLIN_GENERATED_OPTION_NAME] - fileContent.writeTo(File(kaptKotlinGeneratedDir)) - } catch (e: Exception) { - processingEnv.messager.printMessage( - Kind.ERROR, - "Failed to generate remote feature config check: $e" - ) + private fun generateFeaturesInDevelopment(resolver: Resolver) { + val featuresInDevelopment = + resolver.getSymbolsWithAnnotation("org.wordpress.android.annotation.FeatureInDevelopment") + .filterIsInstance() + .toList() + val featuresInDevelopmentDefaults = featuresInDevelopment + .map { it.simpleName.asString() } + + if(featuresInDevelopmentDefaults.isNotEmpty()) { + FeaturesInDevelopmentDefaultsBuilder(featuresInDevelopmentDefaults).getContent() + .writeTo( + codeGenerator, + aggregating = true, + originatingKSFiles = featuresInDevelopment.map { it.containingFile!! } + ) } } - @Suppress("TooGenericExceptionCaught") - private fun generateFeaturesInDevelopment( - remoteFeatureNames: List - ) { - try { - val fileContent = FeaturesInDevelopmentDefaultsBuilder(remoteFeatureNames).getContent() - - val kaptKotlinGeneratedDir = processingEnv.options[KAPT_KOTLIN_GENERATED_OPTION_NAME] - fileContent.writeTo(File(kaptKotlinGeneratedDir)) - } catch (e: Exception) { - processingEnv.messager.printMessage( - Kind.ERROR, - "Failed to generate remote config check: $e" + private fun generateRemoteFeatureConfigCheck(remoteFeatures: List) { + if(remoteFeatures.isNotEmpty()) { + RemoteFeatureConfigCheckBuilder( + remoteFeatures.filterIsInstance().map { it.asType(emptyList()).toTypeName() } + ).getContent().writeTo( + codeGenerator, + aggregating = true, + originatingKSFiles = remoteFeatures.map { it.containingFile!! } ) } } - - companion object { - const val KAPT_KOTLIN_GENERATED_OPTION_NAME = "kapt.kotlin.generated" - } } diff --git a/libs/processors/src/main/java/org/wordpress/android/processor/RemoteConfigProcessorProvider.kt b/libs/processors/src/main/java/org/wordpress/android/processor/RemoteConfigProcessorProvider.kt new file mode 100644 index 000000000000..81c317430a8b --- /dev/null +++ b/libs/processors/src/main/java/org/wordpress/android/processor/RemoteConfigProcessorProvider.kt @@ -0,0 +1,13 @@ +package org.wordpress.android.processor + +import com.google.devtools.ksp.processing.SymbolProcessor +import com.google.devtools.ksp.processing.SymbolProcessorEnvironment +import com.google.devtools.ksp.processing.SymbolProcessorProvider + +class RemoteConfigProcessorProvider : SymbolProcessorProvider { + override fun create( + environment: SymbolProcessorEnvironment + ): SymbolProcessor { + return RemoteConfigProcessor(environment.codeGenerator) + } +} diff --git a/libs/processors/src/main/resources/META-INF/services/com.google.devtools.ksp.processing.SymbolProcessorProvider b/libs/processors/src/main/resources/META-INF/services/com.google.devtools.ksp.processing.SymbolProcessorProvider new file mode 100644 index 000000000000..1f997f654fad --- /dev/null +++ b/libs/processors/src/main/resources/META-INF/services/com.google.devtools.ksp.processing.SymbolProcessorProvider @@ -0,0 +1 @@ +org.wordpress.android.processor.RemoteConfigProcessorProvider diff --git a/libs/processors/src/test/kotlin/org/wordpress/android/processor/RemoteConfigProcessorTest.kt b/libs/processors/src/test/kotlin/org/wordpress/android/processor/RemoteConfigProcessorTest.kt index b4ee3385c125..aa99781c26ac 100644 --- a/libs/processors/src/test/kotlin/org/wordpress/android/processor/RemoteConfigProcessorTest.kt +++ b/libs/processors/src/test/kotlin/org/wordpress/android/processor/RemoteConfigProcessorTest.kt @@ -2,6 +2,8 @@ package org.wordpress.android.processor import com.tschuchort.compiletesting.KotlinCompilation import com.tschuchort.compiletesting.SourceFile +import com.tschuchort.compiletesting.kspWithCompilation +import com.tschuchort.compiletesting.symbolProcessorProviders import org.assertj.core.api.Assertions.assertThat import org.jetbrains.kotlin.utils.addToStdlib.cast import org.junit.Test @@ -32,12 +34,7 @@ class RemoteConfigProcessorTest { ) // when - val result = compile( - listOf( - remoteFieldA, - featureA, /* adding a feature, as without it, annotation processor won't start */ - ) - ) + val result = compile(listOf(remoteFieldA)) // then assertThat(result.exitCode).isEqualTo(KotlinCompilation.ExitCode.OK) @@ -102,12 +99,7 @@ class RemoteConfigProcessorTest { ) // when - val result = compile( - listOf( - experiment, - featureA, /* adding a feature, as without it, annotation processor won't start */ - ) - ) + val result = compile(listOf(experiment)) // then @@ -124,7 +116,8 @@ class RemoteConfigProcessorTest { private fun compile(src: List) = KotlinCompilation().apply { sources = src + fakeAppConfig - annotationProcessors = listOf(RemoteConfigProcessor()) + symbolProcessorProviders = listOf(RemoteConfigProcessorProvider()) + kspWithCompilation = true inheritClassPath = true messageOutputStream = System.out }.compile() diff --git a/settings.gradle b/settings.gradle index c2109be8a27c..73393c25cdf2 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,5 +1,6 @@ pluginManagement { - gradle.ext.kotlinVersion = '1.9.10' + gradle.ext.kotlinVersion = '1.9.22' + gradle.ext.kspVersion = '1.9.22-1.0.17' gradle.ext.agpVersion = '8.1.0' gradle.ext.googleServicesVersion = '4.3.15' gradle.ext.navigationVersion = '2.5.3' @@ -27,6 +28,7 @@ pluginManagement { id 'com.automattic.android.measure-builds' version gradle.ext.measureBuildsVersion id "org.jetbrains.kotlinx.kover" version gradle.ext.koverVersion id "com.google.dagger.hilt.android" version gradle.ext.daggerVersion + id "com.google.devtools.ksp" version gradle.ext.kspVersion } repositories { maven {