Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add recursive lookup for transitive preview annotations #71

Merged
merged 8 commits into from
Sep 29, 2023
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package com.emergetools.snapshots.sample.ui

@LocalePreviews
@FontScalePreviews
annotation class SnapshotTestingPreview
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,8 @@ fun TextRowWithIcon(
}

@Preview
@FontScalePreviews
@LocalePreviews
@FontScalePreviews
@Composable
fun TextRowWithIconPreviewFromMain() {
TextRowWithIcon(
Expand All @@ -35,16 +35,25 @@ fun TextRowWithIconPreviewFromMain() {
)
}

@FontScalePreviews
@LocalePreviews
@FontScalePreviews
@Composable
fun TextRowWithIconPreviewFromMainJustMultiPreview() {
fun TextRowWithIconPreviewFromMainJustStackedMultiPreview() {
TextRowWithIcon(
titleText = stringResource(com.emergetools.snapshots.sample.R.string.sample_title),
subtitleText = stringResource(com.emergetools.snapshots.sample.R.string.sample_subtitle)
)
}

@SnapshotTestingPreview
@Composable
fun TextRowWithIconPreviewFromMainJustSnapshotTestingPreview() {
TextRowWithIcon(
titleText = "Title SnapshotTestingPreview",
subtitleText = "Subtitle SnapshotTestingPreview"
)
}

// Should not be snapshotted as this is marked to be ignored
@Preview
@IgnoreEmergeSnapshot
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@ import com.emergetools.snapshots.processor.preview.ComposablePreviewSnapshotBuil
import com.emergetools.snapshots.processor.preview.ComposablePreviewSnapshotBuilder.addEmergeSnapshotRuleProperty
import com.emergetools.snapshots.processor.preview.ComposablePreviewSnapshotBuilder.addPreviewConfigProperty
import com.emergetools.snapshots.processor.utils.COMPOSE_PREVIEW_ANNOTATION_NAME
import com.emergetools.snapshots.processor.utils.functionsWithMultiPreviewAnnotation
import com.emergetools.snapshots.processor.utils.functionsWithMultiPreviewAnnotations
import com.emergetools.snapshots.processor.utils.functionsWithPreviewAnnotation
import com.emergetools.snapshots.processor.utils.getMultiPreviewAnnotations
import com.emergetools.snapshots.processor.utils.getSymbolsWithMultiPreviewAnnotations
import com.emergetools.snapshots.processor.utils.putOrAppend
import com.emergetools.snapshots.shared.ComposePreviewSnapshotConfig
import com.google.devtools.ksp.KspExperimental
Expand Down Expand Up @@ -50,12 +50,12 @@ class PreviewProcessor(
val symbolsWithPreviewAnnotations = resolver
.getSymbolsWithAnnotation(COMPOSE_PREVIEW_ANNOTATION_NAME)
.toList()
val symbolsWithMultiPreviewAnnotations = resolver.getMultiPreviewAnnotations()
val symbolsWithMultiPreviewAnnotations = resolver.getSymbolsWithMultiPreviewAnnotations()

val previewAnnotatedFunctions = symbolsWithPreviewAnnotations
.functionsWithPreviewAnnotation()
val multiPreviewAnnotatedFunctions = symbolsWithMultiPreviewAnnotations
.functionsWithMultiPreviewAnnotation(resolver)
.functionsWithMultiPreviewAnnotations(resolver)

val previewFunctionMap = buildMap {
putOrAppend(previewAnnotatedFunctions)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,10 @@ import com.emergetools.snapshots.processor.preview.ComposePreviewUtils.getUnique
import com.emergetools.snapshots.processor.preview.ComposePreviewUtils.getUniqueSnapshotConfigsFromPreviewAnnotations
import com.emergetools.snapshots.shared.ComposePreviewSnapshotConfig
import com.google.devtools.ksp.processing.Resolver
import com.google.devtools.ksp.symbol.ClassKind
import com.google.devtools.ksp.symbol.KSAnnotated
import com.google.devtools.ksp.symbol.KSAnnotation
import com.google.devtools.ksp.symbol.KSClassDeclaration
import com.google.devtools.ksp.symbol.KSFunctionDeclaration
import com.squareup.kotlinpoet.ksp.toTypeName

const val COMPOSE_PREVIEW_ANNOTATION_NAME =
"androidx.compose.ui.tooling.preview.Preview"
Expand All @@ -18,26 +17,20 @@ fun List<KSAnnotated>.functionsWithPreviewAnnotation(): Map<KSFunctionDeclaratio
.associateWith { getUniqueSnapshotConfigsFromPreviewAnnotations(it) }
}

fun List<KSAnnotated>.functionsWithMultiPreviewAnnotation(
fun List<KSAnnotated>.functionsWithMultiPreviewAnnotations(
resolver: Resolver,
): Map<KSFunctionDeclaration, List<ComposePreviewSnapshotConfig>> {
val uniqueSnapshotConfigs = filterIsInstance<KSClassDeclaration>()
.filter { it.classKind == ClassKind.ANNOTATION_CLASS }
.flatMap { annotation ->
val multiPreviewAnnotationPreviewAnnotations = annotation.annotations.filter {
it.annotationType.resolve().declaration.qualifiedName?.asString() == COMPOSE_PREVIEW_ANNOTATION_NAME
val uniqueSnapshotConfigs = filterIsInstance<KSFunctionDeclaration>()
.map { function ->
val allPreviewAnnotations = function.annotations.flatMap {
resolver.findAllDirectOrTransitivePreviewAnnotations(
it
)
}.toList()

val fqn = annotation.asType(emptyList()).toTypeName()
resolver
.getSymbolsWithAnnotation(fqn.toString())
.filterIsInstance<KSFunctionDeclaration>()
.map { function ->
function to getUniqueSnapshotConfigsFromMultiPreviewAnnotation(
annotations = multiPreviewAnnotationPreviewAnnotations,
previewFunction = function,
)
}
function to getUniqueSnapshotConfigsFromMultiPreviewAnnotation(
annotations = allPreviewAnnotations,
previewFunction = function,
)
}

// The same function declaration can show up multiple times, so ensure the values are merged together
Expand All @@ -49,30 +42,88 @@ fun List<KSAnnotated>.functionsWithMultiPreviewAnnotation(
return mergedConfigs
}

fun Resolver.getMultiPreviewAnnotations(): List<KSClassDeclaration> {
// Find all symbols with annotations and map to the annotation class declarations
val annotationClassDecls = getAllFiles()
.flatMap { it.declarations }
.filter { it.annotations.count() > 0 }
.flatMap { symbol ->
symbol.annotations.mapNotNull { annotation ->
val annotationQN = annotation.annotationType.resolve().declaration.qualifiedName
annotationQN?.let { qualifiedName ->
getClassDeclarationByName(qualifiedName)
}
}
fun Resolver.getSymbolsWithMultiPreviewAnnotations(): List<KSAnnotated> {
return getMultiPreviewAnnotations()
.mapNotNull { annotation ->
val annotationQN = annotation.annotationType.resolve().declaration.qualifiedName
annotationQN?.let { getSymbolsWithAnnotation(it.asString()) }
}
.filter { it.classKind == ClassKind.ANNOTATION_CLASS }
.toSet()
.flatMap { it }
}

// Of the annotation classes we found, take those that themselves have a preview annotation.
// We can assume these are multi-preview annotations.
return annotationClassDecls
fun Resolver.getMultiPreviewAnnotations(): List<KSAnnotation> {
return getAllFiles()
.flatMap { it.declarations }
.flatMap { it.annotations }
.toSet()
.filter {
it.annotations.any { annotation ->
annotation.annotationType.resolve().declaration.qualifiedName?.asString() == COMPOSE_PREVIEW_ANNOTATION_NAME
}
val annotationQN = it.annotationType.resolve().declaration.qualifiedName
val annotationClassDecl =
annotationQN?.let { qualifiedName -> getClassDeclarationByName(qualifiedName) }
annotationClassDecl?.let { classDecl -> hasDirectOrTransitivePreviewAnnotation(classDecl) }
?: false
}
.toList()
.sortedBy { it.simpleName.asString() }
.sortedBy { it.shortName.asString() }
}

fun Resolver.hasDirectOrTransitivePreviewAnnotation(
declaration: KSAnnotated,
seenAnnotations: MutableSet<KSAnnotated> = mutableSetOf(),
): Boolean {
if (declaration in seenAnnotations) {
return false
}

val hasPreviewAnnotation = declaration.annotations.any {
it.annotationType.resolve().declaration.qualifiedName?.asString() ==
COMPOSE_PREVIEW_ANNOTATION_NAME
}
if (hasPreviewAnnotation) {
return true
}

seenAnnotations.add(declaration)

return declaration.annotations.any { annotation ->
val annotationQualifiedName = annotation.annotationType.resolve().declaration.qualifiedName
val classDeclaration = annotationQualifiedName?.let { getClassDeclarationByName(it) }
classDeclaration?.let {
hasDirectOrTransitivePreviewAnnotation(classDeclaration, seenAnnotations)
} ?: false
}
}

fun Resolver.findAllDirectOrTransitivePreviewAnnotations(
annotation: KSAnnotation,
seenAnnotations: MutableSet<KSClassDeclaration> = mutableSetOf(),
): List<KSAnnotation> {
val classDeclaration = annotation.annotationType.resolve().declaration.qualifiedName?.let {
getClassDeclarationByName(
it
)
}
val isPreviewAnnotation =
classDeclaration?.qualifiedName?.asString() == COMPOSE_PREVIEW_ANNOTATION_NAME

// Annotations can recursively reference each other so be sure to have a base recursion case
// @Preview itself can't have a recursive relation so we can exclude them from our check
if (classDeclaration == null || (classDeclaration in seenAnnotations && !isPreviewAnnotation)) {
return emptyList()
}

seenAnnotations.add(classDeclaration)

val currentPreviewAnnotations =
if (classDeclaration.qualifiedName?.asString() == COMPOSE_PREVIEW_ANNOTATION_NAME) {
listOf(annotation)
} else {
emptyList()
}

val nestedPreviewAnnotations = classDeclaration.annotations.flatMap {
findAllDirectOrTransitivePreviewAnnotations(it, seenAnnotations)
}

return currentPreviewAnnotations + nestedPreviewAnnotations
}
Loading