Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion WordPress/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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'
Expand Down
3 changes: 2 additions & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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'
Expand Down
9 changes: 5 additions & 4 deletions libs/processors/build.gradle
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
plugins {
id "org.jetbrains.kotlin.jvm"
id "org.jetbrains.kotlin.kapt"
id "org.jetbrains.kotlinx.kover"
}

Expand All @@ -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"
Expand Down
Original file line number Diff line number Diff line change
@@ -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<out TypeElement>?, 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<TypeName>()
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<KSAnnotated> {
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<String, String>
) {
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<KSAnnotated>) {
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,
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All output files are set to aggregating strategy for incremental processing. They have to, as we gather annotations from all available files in the codebase.

See https://kotlinlang.org/docs/ksp-incremental.html#aggregating-vs-isolating for more details.

originatingKSFiles = remoteFeatures.map { it.containingFile!! }
)
}
}

@Suppress("TooGenericExceptionCaught", "SwallowedException")
private fun generateRemoteFieldConfigDefaults(
remoteConfigDefaults: Map<String, String>
) {
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<TypeName>
) {
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<KSClassDeclaration>()
.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<String>
) {
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<KSAnnotated>) {
if(remoteFeatures.isNotEmpty()) {
RemoteFeatureConfigCheckBuilder(
remoteFeatures.filterIsInstance<KSClassDeclaration>().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"
}
}
Original file line number Diff line number Diff line change
@@ -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)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
org.wordpress.android.processor.RemoteConfigProcessorProvider
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -32,12 +34,7 @@ class RemoteConfigProcessorTest {
)

// when
val result = compile(
listOf(
remoteFieldA,
featureA, /* adding a feature, as without it, annotation processor won't start */
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice that we are able to fix this 😄

)
)
val result = compile(listOf(remoteFieldA))

// then
assertThat(result.exitCode).isEqualTo(KotlinCompilation.ExitCode.OK)
Expand Down Expand Up @@ -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

Expand All @@ -124,7 +116,8 @@ class RemoteConfigProcessorTest {

private fun compile(src: List<SourceFile>) = KotlinCompilation().apply {
sources = src + fakeAppConfig
annotationProcessors = listOf(RemoteConfigProcessor())
symbolProcessorProviders = listOf(RemoteConfigProcessorProvider())
kspWithCompilation = true
inheritClassPath = true
messageOutputStream = System.out
}.compile()
Expand Down
4 changes: 3 additions & 1 deletion settings.gradle
Original file line number Diff line number Diff line change
@@ -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'
Expand Down Expand Up @@ -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 {
Expand Down