Skip to content

Commit

Permalink
Add generators (#2)
Browse files Browse the repository at this point in the history
  • Loading branch information
illarionov authored Jan 12, 2024
1 parent ee57dbc commit 8ed2059
Show file tree
Hide file tree
Showing 47 changed files with 2,817 additions and 10 deletions.
6 changes: 0 additions & 6 deletions .idea/copyright/anvil-codegen

This file was deleted.

6 changes: 6 additions & 0 deletions .idea/copyright/anvil_codegen.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions .idea/copyright/profiles_settings.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

31 changes: 31 additions & 0 deletions activity/generator/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
/*
* Copyright (c) 2024, the pixnews-anvil-codegen project authors and contributors.
* Please see the AUTHORS file for details.
* Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
*/

plugins {
id("ru.pixnews.anvil.codegen.build-logic.project.kotlin.library")
id("ru.pixnews.anvil.codegen.build-logic.project.test")
id("ru.pixnews.anvil.codegen.build-logic.project.publish")
kotlin("kapt")
}

group = "ru.pixnews.anvil.codegen.activity.generator"
version = "0.1-SNAPSHOT"

dependencies {
api(libs.anvil.compiler.api)
implementation(libs.anvil.compiler.utils)
implementation(libs.kotlinpoet) { exclude(module = "kotlin-reflect") }
implementation(projects.common)

compileOnly(libs.auto.service.annotations)
kapt(libs.auto.service.compiler)

testImplementation(libs.anvil.annotations.optional)
testImplementation(libs.assertk)
testImplementation(libs.dagger)
testImplementation(projects.testUtils)
testImplementation(testFixtures(libs.anvil.compiler.utils))
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
/*
* Copyright (c) 2024, the pixnews-anvil-codegen project authors and contributors.
* Please see the AUTHORS file for details.
* Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
*/

package ru.pixnews.anvil.codegen.activity.generator

import com.google.auto.service.AutoService
import com.squareup.anvil.compiler.api.AnvilContext
import com.squareup.anvil.compiler.api.CodeGenerator
import com.squareup.anvil.compiler.api.GeneratedFile
import com.squareup.anvil.compiler.api.createGeneratedFile
import com.squareup.anvil.compiler.internal.buildFile
import com.squareup.anvil.compiler.internal.reference.ClassReference
import com.squareup.anvil.compiler.internal.reference.asClassName
import com.squareup.anvil.compiler.internal.reference.classAndInnerClassReferences
import com.squareup.anvil.compiler.internal.reference.generateClassName
import com.squareup.anvil.compiler.internal.safePackageString
import com.squareup.kotlinpoet.AnnotationSpec
import com.squareup.kotlinpoet.ClassName
import com.squareup.kotlinpoet.FileSpec
import com.squareup.kotlinpoet.FunSpec
import com.squareup.kotlinpoet.KModifier
import com.squareup.kotlinpoet.ParameterizedTypeName.Companion.parameterizedBy
import com.squareup.kotlinpoet.TypeSpec
import com.squareup.kotlinpoet.WildcardTypeName
import org.jetbrains.kotlin.descriptors.ModuleDescriptor
import org.jetbrains.kotlin.name.FqName
import org.jetbrains.kotlin.psi.KtFile
import ru.pixnews.anvil.codegen.common.classname.DaggerClassName
import ru.pixnews.anvil.codegen.common.classname.PixnewsClassName
import ru.pixnews.anvil.codegen.common.fqname.FqNames
import ru.pixnews.anvil.codegen.common.util.checkClassExtendsType
import ru.pixnews.anvil.codegen.common.util.contributesToAnnotation
import java.io.File

@AutoService(CodeGenerator::class)
public class ContributesActivityCodeGenerator : CodeGenerator {
override fun isApplicable(context: AnvilContext): Boolean = true

override fun generateCode(
codeGenDir: File,
module: ModuleDescriptor,
projectFiles: Collection<KtFile>,
): Collection<GeneratedFile> {
return projectFiles
.classAndInnerClassReferences(module)
.filter { it.isAnnotatedWith(FqNames.contributesActivity) }
.map { generateActivityModule(it, codeGenDir) }
.toList()
}

private fun generateActivityModule(
annotatedClass: ClassReference,
codeGenDir: File,
): GeneratedFile {
annotatedClass.checkClassExtendsType(activityFqName)

val moduleClassId = annotatedClass.generateClassName(suffix = "_ActivityModule")
val generatedPackage = moduleClassId.packageFqName.safePackageString()
val moduleClassName = moduleClassId.relativeClassName.asString()

val moduleInterfaceSpec = TypeSpec.interfaceBuilder(moduleClassName)
.addAnnotation(DaggerClassName.module)
.addAnnotation(contributesToAnnotation(PixnewsClassName.activityScope))
.addFunction(generateBindMethod(annotatedClass))
.build()

val content = FileSpec.buildFile(generatedPackage, moduleClassName) {
addType(moduleInterfaceSpec)
}
return createGeneratedFile(codeGenDir, generatedPackage, moduleClassName, content)
}

private fun generateBindMethod(
annotatedClass: ClassReference,
): FunSpec {
val activityClass = annotatedClass.asClassName()

// MembersInjector<out Activity>
val returnType = DaggerClassName.membersInjector
.parameterizedBy(WildcardTypeName.producerOf(activityClassName))

return FunSpec.builder("binds${annotatedClass.shortName}Injector")
.addModifiers(KModifier.ABSTRACT)
.addAnnotation(DaggerClassName.binds)
.addAnnotation(DaggerClassName.intoMap)
.addAnnotation(
AnnotationSpec
.builder(PixnewsClassName.activityMapKey)
.addMember("activityClass = %T::class", activityClass)
.build(),
)
.addAnnotation(
AnnotationSpec
.builder(PixnewsClassName.singleIn)
.addMember("%T::class", PixnewsClassName.activityScope)
.build(),
)
.addParameter("target", DaggerClassName.membersInjector.parameterizedBy(activityClass))
.returns(returnType)
.build()
}

private companion object {
private val activityClassName = ClassName("android.app", "Activity")
private val activityFqName = FqName("android.app.Activity")
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
/*
* Copyright (c) 2024, the pixnews-anvil-codegen project authors and contributors.
* Please see the AUTHORS file for details.
* Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
*/

package ru.pixnews.anvil.codegen.activity.generator

import assertk.assertThat
import assertk.assertions.containsExactly
import assertk.assertions.containsExactlyInAnyOrder
import assertk.assertions.isEqualTo
import com.squareup.anvil.annotations.ContributesTo
import com.squareup.anvil.annotations.optional.SingleIn
import com.squareup.anvil.compiler.internal.testing.compileAnvil
import com.tschuchort.compiletesting.JvmCompilationResult
import com.tschuchort.compiletesting.KotlinCompilation.ExitCode.OK
import dagger.Binds
import dagger.MembersInjector
import dagger.multibindings.IntoMap
import org.jetbrains.kotlin.compiler.plugin.ExperimentalCompilerApi
import org.junit.jupiter.api.BeforeAll
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.TestInstance
import org.junit.jupiter.api.TestInstance.Lifecycle
import org.junit.jupiter.api.fail
import ru.pixnews.anvil.codegen.common.classname.PixnewsClassName
import ru.pixnews.anvil.codegen.testutils.haveAnnotation
import ru.pixnews.anvil.codegen.testutils.loadClass

@OptIn(ExperimentalCompilerApi::class)
@TestInstance(Lifecycle.PER_CLASS)
class ContributesActivityCodeGeneratorTest {
private val generatedModuleName = "com.test.TestActivity_ActivityModule"
private lateinit var compilationResult: JvmCompilationResult

@BeforeAll
@Suppress("LOCAL_VARIABLE_EARLY_DECLARATION")
fun setup() {
val activityDiStubs = """
package ru.pixnews.foundation.di.ui.base.activity
import android.app.Activity
import dagger.MapKey
import kotlin.reflect.KClass
public abstract class ActivityScope private constructor()
public annotation class ActivityMapKey(val activityClass: KClass<out Activity>)
public annotation class ContributesActivity
""".trimIndent()

val androidActivityStub = """
package android.app
open class Activity
""".trimIndent()

val testActivity = """
package com.test
import android.app.Activity
import ru.pixnews.foundation.di.ui.base.activity.ContributesActivity
@ContributesActivity
class TestActivity : Activity()
""".trimIndent()

compilationResult = compileAnvil(
sources = arrayOf(
activityDiStubs,
androidActivityStub,
testActivity,
),
)
}

@Test
fun `Dagger module should be generated`() {
assertThat(compilationResult.exitCode).isEqualTo(OK)
}

@Test
fun `Generated module should have correct annotations`() {
val clazz = compilationResult.classLoader.loadClass(generatedModuleName)
val activityScopeClass = compilationResult.classLoader.loadClass(PixnewsClassName.activityScope)
assertThat(clazz).haveAnnotation(ContributesTo::class.java)

assertThat(clazz.getAnnotation(ContributesTo::class.java).scope.java)
.isEqualTo(activityScopeClass)
}

@Test
fun `Generated module should have correct binding method`() {
val moduleClass = compilationResult.classLoader.loadClass(generatedModuleName)
val activityMapKey = compilationResult.classLoader.loadClass(PixnewsClassName.activityMapKey)

val provideMethod = moduleClass.declaredMethods.firstOrNull {
it.name == "bindsTestActivityInjector"
} ?: fail("no bindsTestActivityInjector method")

assertThat(provideMethod.returnType).isEqualTo(MembersInjector::class.java)
assertThat(provideMethod.parameterTypes)
.containsExactly(MembersInjector::class.java)
assertThat(
provideMethod.annotations.map(Annotation::annotationClass),
).containsExactlyInAnyOrder(
Binds::class,
IntoMap::class,
SingleIn::class,
activityMapKey.kotlin,
)
}
}
17 changes: 17 additions & 0 deletions activity/inject/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
/*
* Copyright (c) 2024, the pixnews-anvil-codegen project authors and contributors.
* Please see the AUTHORS file for details.
* Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
*/

plugins {
id("ru.pixnews.anvil.codegen.build-logic.project.kotlin.library")
id("ru.pixnews.anvil.codegen.build-logic.project.test")
id("ru.pixnews.anvil.codegen.build-logic.project.publish")
}

group = "ru.pixnews.anvil.codegen.activity.inject"
version = "0.1-SNAPSHOT"

dependencies {
}
1 change: 1 addition & 0 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
plugins {
alias(libs.plugins.gradle.maven.publish.plugin.base) apply false
alias(libs.plugins.kotlin.jvm) apply false
alias(libs.plugins.kotlin.kapt) apply false
alias(libs.plugins.kotlinx.binary.compatibility.validator) apply false
id("ru.pixnews.anvil.codegen.build-logic.project.kotlin.library") apply false
id("ru.pixnews.anvil.codegen.build-logic.project.publish") apply false
Expand Down
19 changes: 19 additions & 0 deletions common/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
/*
* Copyright (c) 2024, the pixnews-anvil-codegen project authors and contributors.
* Please see the AUTHORS file for details.
* Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
*/

plugins {
id("ru.pixnews.anvil.codegen.build-logic.project.kotlin.library")
id("ru.pixnews.anvil.codegen.build-logic.project.test")
id("ru.pixnews.anvil.codegen.build-logic.project.publish")
}

dependencies {
api(libs.kotlinpoet) {
exclude(module = "kotlin-reflect")
}
api(libs.anvil.compiler.api)
api(libs.anvil.compiler.utils)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
/*
* Copyright (c) 2024, the pixnews-anvil-codegen project authors and contributors.
* Please see the AUTHORS file for details.
* Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
*/

package ru.pixnews.anvil.codegen.common.classname

import com.squareup.kotlinpoet.ClassName

public object AndroidClassName {
@JvmStatic
public val context: ClassName = ClassName("android.content", "Context")

@JvmStatic
public val savedStateHandle: ClassName = ClassName("androidx.lifecycle", "SavedStateHandle")

@JvmStatic
public val workerParameters: ClassName = ClassName("androidx.work", "WorkerParameters")
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
/*
* Copyright (c) 2024, the pixnews-anvil-codegen project authors and contributors.
* Please see the AUTHORS file for details.
* Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
*/

package ru.pixnews.anvil.codegen.common.classname

import com.squareup.kotlinpoet.ClassName

public object AnvilClassName {
private const val ANVIL_ANNOTATIONS_PACKAGE = "com.squareup.anvil.annotations"

@JvmStatic
public val contributesMultibinding: ClassName = ClassName(ANVIL_ANNOTATIONS_PACKAGE, "ContributesMultibinding")

@JvmStatic
public val contributesTo: ClassName = ClassName(ANVIL_ANNOTATIONS_PACKAGE, "ContributesTo")
}
Loading

0 comments on commit 8ed2059

Please sign in to comment.