Skip to content

Commit

Permalink
Improve documentation (#15)
Browse files Browse the repository at this point in the history
Closes #4
Closes #5
  • Loading branch information
outadoc authored Jan 21, 2022
1 parent 91cc9a5 commit 280e12c
Show file tree
Hide file tree
Showing 17 changed files with 175 additions and 107 deletions.
53 changes: 52 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1 +1,52 @@
# toothpick-compiler-ksp
# KSP Processor for Toothpick ![Release](https://jitpack.io/v/fr.outadoc/toothpick-compiler-ksp.svg)

## Background

[Toothpick](https://github.com/stephanenicolas/toothpick) is a Dependency Injection library for Java & Kotlin.

An important part of Toothpick is its ability to generate code at compile-time to avoid doing expensive reflection at
runtime. This is handled by an *annotation processor*, which uses Java's `apt` and Kotlin's `kapt` APIs.

Recently, Google has released an alternative, modern annotation processor, built for Kotlin and compatible with Java
projects: KSP (Kotlin Symbol Processor). This API is much faster than `kapt` on Kotlin projects.

## Goals

This projects aims to reimplement Toothpick's annotation processor with Kotlin-based technologies. It now uses KSP for
better build performance, and generates Kotlin code for improved type safety.

> **Important note:** This processor is mostly a drop-in replacement for the official `kapt` processor, but compatibility was *not* a main goal. You might need to make small changes to your code in order to build using this module.
>
> This is in part because of fundamental differences in the way KSP models Kotlin code compared to `kapt`, and in part because of the differences in generated Kotlin code vs. Java code (no `package` visibility modifier, for example.)
## Setup

```kotlin
plugins {
// Remove this if you don't have any kapt(...) processors left in your dependencies:
// kotlin("kapt") version "..."

// Use the version that matches your Kotlin version!
id("com.google.devtools.ksp") version "1.6.10-1.0.2"
}

repositories {
mavenCentral()
maven { url = uri("https://jitpack.io") }
}

dependencies {
// Remove this:
// kapt("com.github.stephanenicolas.toothpick:toothpick-compiler:...")

ksp("fr.outadoc.toothpick-compiler-ksp:toothpick-compiler-ksp:0.1")
}

ksp {
// If you need to, specify some extra options.
// See ToothpickOptions.kt for documentation.
arg("option1", "value1")
arg("option2", "value2")
// ...
}
```
Original file line number Diff line number Diff line change
Expand Up @@ -32,21 +32,23 @@ data class ToothpickOptions(
),
val crashWhenNoFactoryCanBeCreated: Boolean = false,
val crashWhenInjectedMethodIsNotPackageVisible: Boolean = false,
val debugLogOriginatingElements: Boolean = false
val verboseLogging: Boolean = false
) {

companion object {

/**
* The name of the annotation processor option to exclude classes from the creation of member
* scopes & factories. Exclude filters are java regex, multiple entries are comma separated.
* scopes & factories. Exclude filters are Java regexes, multiple entries are comma separated.
*/
const val Excludes = "toothpick_excludes"

/**
* The name of the annotation processor option to let TP know about custom scope annotation
* classes. This option is needed only in the case where a custom scope annotation is used on a
* class, and this class doesn't use any annotation processed out of the box by TP (i.e.
* classes.
*
* This option is needed only in the case where a custom scope annotation is used on a
* class, and this class doesn't use any annotation processed out-of-the-box by TP (i.e.
* javax.inject.* annotations). If you use custom scope annotations, it is a good practice to
* always use this option so that developers can use the new scope annotation in a very free way
* without having to consider the annotation processing internals.
Expand All @@ -55,19 +57,28 @@ data class ToothpickOptions(

/**
* The name of the annotation processor option to make the TP annotation processor crash when it
* can't generate a factory for a class. By default the behavior is not to crash but emit a
* warning. Passing the value `true` crashes the build instead.
* can't generate a factory for a class.
*
* By default, the behavior is not to crash but emit a warning.
* Passing the value `true` crashes the build instead.
*/
const val CrashWhenNoFactoryCanBeCreated = "toothpick_crash_when_no_factory_can_be_created"

/**
* The name of the annotation processor option to make the TP annotation processor crash when it
* detects an annotated method but with a non package-private visibility. By default the behavior
* is not to crash but emit a warning. Passing the value `true` crashes the build instead.
* detects an annotated method but with a non package-private visibility.
*
* By default, the behavior is not to crash but emit a warning.
* Passing the value `true` crashes the build instead.
*/
const val CrashWhenInjectedMethodIsNotPackageVisible = "toothpick_crash_when_injected_method_is_not_package"

const val DebugLogOriginatingElements = "toothpick_debug_log_originating_elements"
/**
* The name of the annotation processor option to log more information than usual.
*
* Might be useful for debugging or test purposes.
*/
const val VerboseLogging = "toothpick_verbose_log"
}
}

Expand All @@ -91,8 +102,8 @@ fun Map<String, String>.readOptions(): ToothpickOptions {
crashWhenInjectedMethodIsNotPackageVisible = this[ToothpickOptions.CrashWhenInjectedMethodIsNotPackageVisible]
?.toBoolean()
?: default.crashWhenInjectedMethodIsNotPackageVisible,
debugLogOriginatingElements = this[ToothpickOptions.DebugLogOriginatingElements]
verboseLogging = this[ToothpickOptions.VerboseLogging]
?.toBoolean()
?: default.debugLogOriginatingElements
?: default.verboseLogging
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,6 @@ import toothpick.compiler.common.generators.warn
import java.io.IOException
import javax.inject.Inject

/** Base processor class. */
@OptIn(KspExperimental::class, KotlinPoetKspPreview::class)
abstract class ToothpickProcessor(
processorOptions: Map<String, String>,
Expand All @@ -66,11 +65,11 @@ abstract class ToothpickProcessor(
}

protected fun KSType.isValidInjectedType(node: KSNode, qualifiedName: String?): Boolean {
return if (!isValidInjectedElementKind()) false
return if (!isValidInjectedClassKind()) false
else !isProviderOrLazy() || isValidProviderOrLazy(node, qualifiedName)
}

private fun KSType.isValidInjectedElementKind(): Boolean =
private fun KSType.isValidInjectedClassKind(): Boolean =
(declaration as? KSClassDeclaration)?.classKind in validInjectableTypes

private fun KSType.isValidProviderOrLazy(node: KSNode, qualifiedName: String?): Boolean {
Expand Down Expand Up @@ -178,9 +177,9 @@ abstract class ToothpickProcessor(
}

/**
* Checks if `element` has a @SuppressWarning("`warningSuppressString`") annotation.
* Checks if this node has a @SuppressWarning([warningSuppressString]) annotation.
*
* @param element the element to check if the warning is suppressed.
* @receiver the node to check if the warning is suppressed.
* @param warningSuppressString the value of the SuppressWarning annotation.
* @return true is the injectable warning is suppressed, false otherwise.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,18 @@ package toothpick.compiler.common.generators

import com.squareup.kotlinpoet.ClassName

/**
* The name of the generated factory class for a given class.
*/
val ClassName.factoryClassName: ClassName
get() = ClassName(
packageName = packageName,
simpleNames.joinToString("$") + "__Factory"
)

/**
* The name of the generated member injector class for a given class.
*/
val ClassName.memberInjectorClassName: ClassName
get() = ClassName(
packageName = packageName,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,14 +20,14 @@ package toothpick.compiler.common.generators
import com.google.devtools.ksp.processing.KSPLogger
import com.google.devtools.ksp.symbol.KSNode

fun KSPLogger.error(message: String, vararg args: Any?) =
error(message.format(*args))
fun KSPLogger.error(node: String, vararg args: Any?) =
error(node.format(*args))

fun KSPLogger.error(element: KSNode?, message: String, vararg args: Any?) =
error(message.format(*args), element)
fun KSPLogger.error(node: KSNode?, message: String, vararg args: Any?) =
error(message.format(*args), node)

fun KSPLogger.warn(element: KSNode?, message: String, vararg args: Any?) =
warn(message.format(*args), element)
fun KSPLogger.warn(node: KSNode?, message: String, vararg args: Any?) =
warn(message.format(*args), node)

fun KSPLogger.info(message: String, vararg args: Any?) =
info(message.format(*args))
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,9 @@ import toothpick.compiler.common.generators.error
import javax.inject.Named
import javax.inject.Qualifier

/**
* Information necessary to identify the parameter of a method or a class's property.
*/
sealed class VariableInjectionTarget(
val memberType: KSType,
val memberName: KSName,
Expand Down Expand Up @@ -107,8 +110,8 @@ sealed class VariableInjectionTarget(
* Lookup both [javax.inject.Qualifier] and [javax.inject.Named] to provide the name
* of an injection.
*
* @param element the element for which a qualifier is to be found.
* @return the name of this element or null if it has no qualifier annotations.
* @receiver the node for which a qualifier is to be found.
* @return the name of this injection, or null if it has no qualifier annotations.
*/
private fun KSAnnotated.findQualifierName(logger: KSPLogger?): String? {
val qualifierAnnotationNames = annotations
Expand All @@ -133,12 +136,12 @@ sealed class VariableInjectionTarget(
}

/**
* Retrieves the type of a field or param. The type can be the type of the parameter in the java
* way (e.g. [b: B], type is [B]; but it can also be the type of a [toothpick.Lazy] or
* [javax.inject.Provider] (e.g. [Lazy<B>], type is [B] not [Lazy]).
* Retrieves the type of a field or param.
*
* @receiver the field or variable element type
* @return the type has defined above.
* @receiver The type to inspect.
* @return Can be the type of a simple instance (e.g. in `b: B`, type is `B`).
* But if the type is [toothpick.Lazy] or [javax.inject.Provider], then we use the type parameter
* (e.g. in `Lazy<B>`, type is `B`, not `Lazy`).
*/
private fun KSType.getInjectedType(): KSType = arguments.first().type!!.resolve()
}
Expand Down
Loading

0 comments on commit 280e12c

Please sign in to comment.