Skip to content
Open
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
5 changes: 4 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,7 @@ local.properties
.gradle

# deepLinkDispatch
dld_match_*.idx
dld_match_*.idx

# Kotlin
.kotlin
8 changes: 4 additions & 4 deletions deeplinkdispatch-base/build.gradle
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import org.jetbrains.kotlin.gradle.dsl.JvmTarget

apply plugin: 'java'
apply plugin: 'kotlin'
apply plugin: 'org.jmailen.kotlinter'
Expand All @@ -17,10 +19,8 @@ dependencies {
testImplementation deps.assertJ
}

tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).all {
kotlinOptions {
jvmTarget = "11"
}
kotlin.compilerOptions {
jvmTarget = JvmTarget.JVM_11
}

checkstyle {
Expand Down
10 changes: 5 additions & 5 deletions deeplinkdispatch-gradle-plugin/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import org.jetbrains.kotlin.gradle.dsl.JvmTarget

apply(from = "$rootDir/dependencies.gradle")
apply(from = "$rootDir/publishing.gradle")

Expand All @@ -20,10 +22,8 @@ java {
targetCompatibility = JavaVersion.VERSION_11
}

tasks.withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompile>().configureEach {
compilerOptions {
jvmTarget.set(org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_11)
}
kotlin.compilerOptions {
jvmTarget = JvmTarget.JVM_11
}

gradlePlugin {
Expand Down Expand Up @@ -53,4 +53,4 @@ tasks.test {
events("passed", "skipped", "failed")
showStandardStreams = true
}
}
}
23 changes: 10 additions & 13 deletions deeplinkdispatch-processor/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,8 @@ apply plugin: 'org.jmailen.kotlinter'
apply plugin: 'checkstyle'
apply from: '../publishing.gradle'

java {
toolchain {
languageVersion = JavaLanguageVersion.of(11)
}
kotlin {
jvmToolchain(11)
}

dependencies {
Expand Down Expand Up @@ -36,14 +34,13 @@ checkstyle {
configProperties = ['checkstyle.cache.file': rootProject.file('build/checkstyle.cache')]
}

tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).all {
kotlinOptions {
jvmTarget = "11"
freeCompilerArgs += "-opt-in=kotlin.RequiresOptIn"
freeCompilerArgs += "-opt-in=androidx.room.compiler.processing.ExperimentalProcessingApi"
freeCompilerArgs += "-opt-in=kotlin.ExperimentalUnsignedTypes"
freeCompilerArgs += "-opt-in=com.squareup.kotlinpoet.javapoet.KotlinPoetJavaPoetPreview"
}
kotlin.compilerOptions {
optIn.addAll(
"kotlin.RequiresOptIn",
"androidx.room.compiler.processing.ExperimentalProcessingApi",
"kotlin.ExperimentalUnsignedTypes",
"com.squareup.kotlinpoet.javapoet.KotlinPoetJavaPoetPreview",
)
}

test {
Expand All @@ -57,4 +54,4 @@ test {
jvmArgs '--add-opens=jdk.compiler/com.sun.tools.javac.processing=ALL-UNNAMED'
jvmArgs '--add-opens=jdk.compiler/com.sun.tools.javac.comp=ALL-UNNAMED'
jvmArgs '--add-opens=jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED'
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@ import androidx.room.compiler.processing.XFiler
import androidx.room.compiler.processing.XMethodElement
import androidx.room.compiler.processing.XProcessingEnv
import androidx.room.compiler.processing.XRoundEnv
import androidx.room.compiler.processing.XType
import androidx.room.compiler.processing.XTypeElement
import androidx.room.compiler.processing.addOriginatingElement
import androidx.room.compiler.processing.writeTo
Expand Down Expand Up @@ -86,10 +85,9 @@ class DeepLinkProcessor(
private val incrementalMetadata: IncrementalMetadata by lazy {
IncrementalMetadata(
incremental = environment.options[OPTION_INCREMENTAL].toBoolean(),
customAnnotations =
customAnnotationNames =
environment.options[OPTION_CUSTOM_ANNOTATIONS]
?.split("|")
?.mapNotNull { environment.findTypeElement(it) }
?.toSet() ?: emptySet(),
)
}
Expand All @@ -104,9 +102,10 @@ class DeepLinkProcessor(

override fun getSupportedAnnotationTypes(): Set<String> =
if (incrementalMetadata.incremental) {
(supportedBaseAnnotations + incrementalMetadata.customAnnotations)
.map { it.qualifiedName }
.toSet()
(
supportedBaseAnnotations.map { it.qualifiedName } +
incrementalMetadata.customAnnotationNames
).toSet()
} else {
setOf("*")
}
Expand All @@ -115,7 +114,7 @@ class DeepLinkProcessor(

override fun getSupportedOptions(): Set<String> {
val supportedOptions =
listOf(
listOfNotNull(
Documentor.DOC_OUTPUT_PROPERTY_NAME,
OPTION_CUSTOM_ANNOTATIONS,
OPTION_INCREMENTAL,
Expand All @@ -124,7 +123,7 @@ class DeepLinkProcessor(
} else {
null
},
).filterNotNull()
)
return supportedOptions.toSet()
}

Expand Down Expand Up @@ -214,7 +213,7 @@ class DeepLinkProcessor(
}

private fun collectDeepLinkElements(
prefixesAndFqn: Map<XType, Array<PrefixAndActivityFqn>>,
prefixesAndFqn: Map<String, Array<PrefixAndActivityFqn>>,
classElementsToProcess: Set<XTypeElement>,
objectElementsToProcess: Set<XTypeElement>,
methodElementsToProcess: Set<XMethodElement>,
Expand All @@ -239,7 +238,7 @@ class DeepLinkProcessor(

private fun mapUrisToDeepLinkAnnotatedElement(
element: XElement,
prefixesAndFqn: Map<XType, Array<PrefixAndActivityFqn>>,
prefixesAndFqn: Map<String, Array<PrefixAndActivityFqn>>,
): List<DeepLinkAnnotatedElement?> =
getAllUrisAndActivityClassesForAnnotatedElement(
element,
Expand Down Expand Up @@ -285,7 +284,7 @@ class DeepLinkProcessor(
"'ActivityAnnotatedElement' or 'HandlerAnnotatedElement'",
)
}
} catch (e: MalformedURLException) {
} catch (_: MalformedURLException) {
environment.messager.printMessage(
kind = Diagnostic.Kind.ERROR,
msg = "Malformed Deep Link URL $uriAndActivityFqn.uri",
Expand All @@ -296,20 +295,20 @@ class DeepLinkProcessor(

private fun getAllUrisAndActivityClassesForAnnotatedElement(
element: XElement,
prefixesAndFqn: Map<XType, Array<PrefixAndActivityFqn>>,
prefixesAndFqn: Map<String, Array<PrefixAndActivityFqn>>,
): List<UriAndActivityFqn> {
val deepLinkAnnotation = element.getAnnotation(DEEP_LINK_CLASS)?.value
val deepLinkAnnotation = element.getAnnotation(DEEP_LINK_CLASS)
return getAllDeeplinkUrIsFromCustomDeepLinksOnElement(
element = element,
prefixesAndActivityFqnMap = prefixesAndFqn,
) +
(deepLinkAnnotation?.value?.toList() ?: emptyList()).map { uri ->
(deepLinkAnnotation?.getAsList<String>("value") ?: emptyList()).map { uri ->
UriAndActivityFqn(
uri = uri,
activityClassFqn = deepLinkAnnotation?.activityClassFqn?.ifEmpty { null },
actions = deepLinkAnnotation?.actions?.toSet() ?: emptySet(),
categories = deepLinkAnnotation?.categories?.toSet() ?: emptySet(),
intentFilterAttributes = deepLinkAnnotation?.intentFilterAttributes?.toSet() ?: emptySet(),
activityClassFqn = (deepLinkAnnotation?.get("activityClassFqn")?.value as? String)?.ifEmpty { null },
actions = deepLinkAnnotation?.getAsList<String>("actions")?.toSet() ?: emptySet(),
categories = deepLinkAnnotation?.getAsList<String>("categories")?.toSet() ?: emptySet(),
intentFilterAttributes = deepLinkAnnotation?.getAsList<String>("intentFilterAttributes")?.toSet() ?: emptySet(),
)
}
}
Expand Down Expand Up @@ -426,7 +425,7 @@ class DeepLinkProcessor(
val annotatedPathParameterNames =
allPathParameters
.mapNotNull {
it.getAnnotation(DeeplinkParam::class)?.value?.name
it.getAnnotation(DeeplinkParam::class)?.get("name")?.value as? String
}.toSet()
val annotatedPathParametersThatAreNotInUrlTemplate =
annotatedPathParameterNames.filter { !templateHostPathSchemePlaceholders.contains(it) }
Expand All @@ -449,10 +448,18 @@ class DeepLinkProcessor(
annotation.qualifiedName == DeeplinkParam::class.qualifiedName
}?.annotationValues
?.any { annotationValue ->
annotationValue.value.toString() == deepLinkParamType.toString()
annotationValue.value.toString() == deepLinkParamType.toAnnotationValue()
} ?: false
}

private fun <T : Enum<T>> Enum<T>.toAnnotationValue(): String =
if (environment.backend == XProcessingEnv.Backend.KSP) {
// KSP appends parent class name to annotation value
"${declaringJavaClass.simpleName}.$this"
} else {
this.toString()
}

private fun verifyObjectElement(element: XTypeElement) {
if (!element.isHandler()) {
throw DeepLinkProcessorException(
Expand All @@ -476,9 +483,9 @@ class DeepLinkProcessor(
}
}

private fun customAnnotationPrefixes(customAnnotations: Set<XTypeElement>): Map<XType, Array<PrefixAndActivityFqn>> =
private fun customAnnotationPrefixes(customAnnotations: Set<XTypeElement>): Map<String, Array<PrefixAndActivityFqn>> =
customAnnotations
.map { customAnnotationTypeElement ->
.associate { customAnnotationTypeElement ->
if (!customAnnotationTypeElement.isAnnotationClass()) {
logError(
element = customAnnotationTypeElement,
Expand All @@ -488,8 +495,8 @@ class DeepLinkProcessor(
val activityClassFqn: String? =
customAnnotationTypeElement
.getAnnotation(DEEP_LINK_SPEC_CLASS)
?.let { xAnnotationBox ->
xAnnotationBox.value.activityClassFqn
?.let { xAnnotation ->
xAnnotation.get("activityClassFqn")?.value as? String
}?.ifEmpty { null }
// The value of prefix is an array, however we do allow prefixes also to exist in a single string (within the array)
// these are separated by a `#` char, which is not allowed to exist in the non path part of the URL without being escaped
Expand All @@ -498,14 +505,14 @@ class DeepLinkProcessor(
// easily configure multiple prefixes that are not hardcoded in the codebase of the app using DLD
val prefixes =
customAnnotationTypeElement
.getArrayAnnotationParameter { it.prefix }
.getArrayAnnotationParameter("prefix")
.map { singlePrefix ->
singlePrefix.split(CUSTOM_ANNOTATION_URL_PREFIX_DELIMITER)
}.flatten()
.toTypedArray()
val actions = customAnnotationTypeElement.getArrayAnnotationParameter { it.actions }
val categories = customAnnotationTypeElement.getArrayAnnotationParameter { it.categories }
val intentFilterAttributes = customAnnotationTypeElement.getArrayAnnotationParameter { it.intentFilterAttributes }
val actions = customAnnotationTypeElement.getArrayAnnotationParameter("actions")
val categories = customAnnotationTypeElement.getArrayAnnotationParameter("categories")
val intentFilterAttributes = customAnnotationTypeElement.getArrayAnnotationParameter("intentFilterAttributes")

if (prefixes.hasEmptyOrNullString()) {
logError(
Expand All @@ -519,7 +526,7 @@ class DeepLinkProcessor(
message = "Prefix property cannot be empty",
)
}
customAnnotationTypeElement.type to
customAnnotationTypeElement.qualifiedName to
prefixes
.map {
PrefixAndActivityFqn(
Expand All @@ -530,10 +537,10 @@ class DeepLinkProcessor(
intentFilterAttributes = intentFilterAttributes.toSet(),
)
}.toTypedArray()
}.toMap()
}

private fun XTypeElement.getArrayAnnotationParameter(actionsExtractor: (DeepLinkSpec) -> Array<String>): Array<String> =
getAnnotation(DEEP_LINK_SPEC_CLASS)?.let { actionsExtractor(it.value) }
private fun XTypeElement.getArrayAnnotationParameter(propertyName: String): Array<String> =
getAnnotation(DEEP_LINK_SPEC_CLASS)?.getAsList<String>(propertyName)?.toTypedArray()
?: emptyArray()

private fun verifyAnnotatedType(
Expand Down Expand Up @@ -567,12 +574,12 @@ class DeepLinkProcessor(
val packagesWithMoreThanOneDeepLinkHandler =
deeplinkHandlerAnnotatedElements.groupBy { it.packageName }.filter { it.value.size > 1 }
if (packagesWithMoreThanOneDeepLinkHandler.isNotEmpty()) {
packagesWithMoreThanOneDeepLinkHandler.forEach { it ->
packagesWithMoreThanOneDeepLinkHandler.forEach {
logError(
element = it.value.first().enclosingTypeElement,
message =
"Only one @DeepLinkHandler annotated element allowed per package!" +
" ${it.key} has ${it.value.joinToString { it.qualifiedName }}.",
" ${it.key} has ${it.value.map(XTypeElement::qualifiedName).sorted().joinToString()}.",
)
}
return false
Expand Down Expand Up @@ -1000,9 +1007,15 @@ class DeepLinkProcessor(

private class IncrementalMetadata(
val incremental: Boolean,
val customAnnotations: Set<XTypeElement>,
val customAnnotationNames: Set<String>,
)

private val IncrementalMetadata.customAnnotations: Set<XTypeElement>
get() =
customAnnotationNames
.mapNotNull { environment.findTypeElement(it) }
.toSet()

companion object {
private const val PACKAGE_NAME = "com.airbnb.deeplinkdispatch"
private const val OPTION_CUSTOM_ANNOTATIONS = "deepLink.customAnnotations"
Expand All @@ -1021,12 +1034,12 @@ class DeepLinkProcessor(
*/
private fun getAllDeeplinkUrIsFromCustomDeepLinksOnElement(
element: XElement,
prefixesAndActivityFqnMap: Map<XType, Array<PrefixAndActivityFqn>>,
prefixesAndActivityFqnMap: Map<String, Array<PrefixAndActivityFqn>>,
): List<UriAndActivityFqn> =
element.findAnnotatedAnnotation<DeepLinkSpec>().flatMap { customAnnotation ->
val suffixes = customAnnotation.getAsList<String>("value")
val prefixesAndActivityFqns =
prefixesAndActivityFqnMap[customAnnotation.type]
prefixesAndActivityFqnMap[customAnnotation.qualifiedName]
?: throw DeepLinkProcessorException(
"Unable to find annotation '${customAnnotation.qualifiedName}' you must" +
" update 'deepLink.customAnnotations' within the build.gradle",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ package com.airbnb.deeplinkdispatch
import androidx.room.compiler.processing.XAnnotation
import androidx.room.compiler.processing.XAnnotationValue
import androidx.room.compiler.processing.XTypeElement
import androidx.room.compiler.processing.get

object ProcessorUtils {
@JvmStatic
Expand Down Expand Up @@ -53,19 +52,10 @@ fun XTypeElement.directlyImplementsInterfaces(fqnList: List<String>): Boolean =

@Suppress("UNCHECKED_CAST")
inline fun <reified T> XAnnotation.getAsList(method: String): List<T> {
val originalList = get<List<T>>(method)
// In new XProcessing versions List values are wrapped in XAnnotationValue but in old versions
// they are the raw type.
return if (originalList.firstOrNull() is XAnnotationValue) {
// TODO: In the next full release of xprocessing we should be able to safely assume
// the list type is always XAnnotationValue and can remove this if/else.
(originalList as List<XAnnotationValue>).map { xAnnotationValue ->
check(xAnnotationValue.value is T) {
"Expected type ${T::class} but got ${xAnnotationValue.value?.javaClass}"
}
xAnnotationValue.value as T
}
} else {
return originalList
val annotationValue = get(method) ?: return emptyList()
// In XProcessing 2.8+, annotation values are wrapped in XAnnotationValue
return when (val value = annotationValue.value) {
is List<*> -> value.mapNotNull { (it as XAnnotationValue).value as? T }
else -> listOf(value as T)
}
}
Loading