diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..710f5c4 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,28 @@ +name: CI + +on: + push: + branches: [main] + pull_request: + branches: [main] + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-java@v4 + with: + java-version: "21" + distribution: temurin + - uses: gradle/actions/setup-gradle@v4 + with: + cache-cleanup: on-success + - run: ./gradlew assemble + - run: ./gradlew check + - uses: dorny/test-reporter@v1 + if: always() + with: + name: Test Results + path: "**/build/test-results/test/*.xml" + reporter: java-junit diff --git a/ksp-test-fixtures/src/test/kotlin/com/faire/ksp/test/ReflectionEquivalenceTest.kt b/ksp-test-fixtures/src/test/kotlin/com/faire/ksp/test/ReflectionEquivalenceTest.kt new file mode 100644 index 0000000..ac04a79 --- /dev/null +++ b/ksp-test-fixtures/src/test/kotlin/com/faire/ksp/test/ReflectionEquivalenceTest.kt @@ -0,0 +1,29 @@ +package com.faire.ksp.test + +import com.faire.ksp.adapters.ksClassDeclaration +import com.faire.ksp.snapshot.snapshots.toSnapshot +import com.faire.ksp.test.fixtures.SimpleDataClass +import com.faire.ksp.test.fixtures.SimpleDataClassKspSnapshot +import com.faire.ksp.test.fixtures.SimpleInterface +import com.faire.ksp.test.fixtures.SimpleInterfaceKspSnapshot +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Test + +class ReflectionEquivalenceTest { + + @Test + fun `SimpleDataClass reflection adapter is equivalent to KSP`() { + val kspSnapshot = SimpleDataClassKspSnapshot.snapshot + val reflectionSnapshot = SimpleDataClass::class.ksClassDeclaration.toSnapshot() + + assertThat(reflectionSnapshot).isEqualTo(kspSnapshot) + } + + @Test + fun `SimpleInterface reflection adapter is equivalent to KSP`() { + val kspSnapshot = SimpleInterfaceKspSnapshot.snapshot + val reflectionSnapshot = SimpleInterface::class.ksClassDeclaration.toSnapshot() + + assertThat(reflectionSnapshot).isEqualTo(kspSnapshot) + } +} diff --git a/ksp-test-fixtures/src/test/kotlin/com/faire/ksp/test/fixtures/SimpleDataClass.kt b/ksp-test-fixtures/src/test/kotlin/com/faire/ksp/test/fixtures/SimpleDataClass.kt new file mode 100644 index 0000000..a7819bc --- /dev/null +++ b/ksp-test-fixtures/src/test/kotlin/com/faire/ksp/test/fixtures/SimpleDataClass.kt @@ -0,0 +1,10 @@ +package com.faire.ksp.test.fixtures + +import com.faire.ksp.snapshot.GenerateSnapshot + +@GenerateSnapshot +data class SimpleDataClass( + val name: String, + val age: Int, + var mutableField: Boolean, +) diff --git a/ksp-test-fixtures/src/test/kotlin/com/faire/ksp/test/fixtures/SimpleInterface.kt b/ksp-test-fixtures/src/test/kotlin/com/faire/ksp/test/fixtures/SimpleInterface.kt new file mode 100644 index 0000000..1010a01 --- /dev/null +++ b/ksp-test-fixtures/src/test/kotlin/com/faire/ksp/test/fixtures/SimpleInterface.kt @@ -0,0 +1,9 @@ +package com.faire.ksp.test.fixtures + +import com.faire.ksp.snapshot.GenerateSnapshot + +@GenerateSnapshot +interface SimpleInterface { + val id: String + fun process(input: Int): Boolean +} diff --git a/test-processor/src/main/kotlin/com/faire/ksp/snapshot/SnapshotLiteralWriter.kt b/test-processor/src/main/kotlin/com/faire/ksp/snapshot/SnapshotLiteralWriter.kt new file mode 100644 index 0000000..16d43d4 --- /dev/null +++ b/test-processor/src/main/kotlin/com/faire/ksp/snapshot/SnapshotLiteralWriter.kt @@ -0,0 +1,75 @@ +package com.faire.ksp.snapshot + +import com.google.devtools.ksp.processing.Resolver +import com.google.devtools.ksp.symbol.KSType +import com.squareup.kotlinpoet.CodeBlock +import com.squareup.kotlinpoet.asClassName +import com.squareup.kotlinpoet.buildCodeBlock + +internal class SnapshotLiteralWriter( + private val resolver: Resolver +) { + fun toLiteralCode(value: Any): CodeBlock { + val qualifiedName = value::class.qualifiedName + ?: error("No qualified name for ${value::class}") + + val ksClass = resolver.getClassDeclarationByName(resolver.getKSNameFromString(qualifiedName)) + ?: error("Cannot resolve $qualifiedName via KSP") + + val constructor = ksClass.primaryConstructor + ?: error("No primary constructor for $qualifiedName") + + val className = value::class.asClassName() + + return buildCodeBlock { + add("%T(\n", className) + indent() + for (param in constructor.parameters) { + val paramName = param.name!!.asString() + val paramType = param.type.resolve() + val propValue = readProperty(value, paramName) + add("$paramName = ") + add(formatValue(propValue, paramType)) + add(",\n") + } + unindent() + add(")") + } + } + + private fun readProperty(instance: Any, name: String): Any? { + val field = instance::class.java.getDeclaredField(name) + field.isAccessible = true + return field.get(instance) + } + + private fun formatValue(value: Any?, type: KSType): CodeBlock { + if (value == null) return CodeBlock.of("null") + + return when (type.declaration.qualifiedName?.asString()) { + "kotlin.String" -> CodeBlock.of("%S", value) + "kotlin.Boolean", "kotlin.Int" -> CodeBlock.of("%L", value) + "kotlin.collections.List" -> { + val list = value as List<*> + if (list.isEmpty()) return CodeBlock.of("listOf()") + val elementType = type.arguments.first().type!!.resolve() + formatList(list, elementType) + } + + else -> toLiteralCode(value) + } + } + + private fun formatList(list: List<*>, elementType: KSType): CodeBlock { + return buildCodeBlock { + add("listOf(\n") + indent() + for (item in list) { + add(formatValue(item, elementType)) + add(",\n") + } + unindent() + add(")") + } + } +} diff --git a/test-processor/src/main/kotlin/com/faire/ksp/snapshot/SnapshotProcessor.kt b/test-processor/src/main/kotlin/com/faire/ksp/snapshot/SnapshotProcessor.kt index 3648e19..033dde9 100644 --- a/test-processor/src/main/kotlin/com/faire/ksp/snapshot/SnapshotProcessor.kt +++ b/test-processor/src/main/kotlin/com/faire/ksp/snapshot/SnapshotProcessor.kt @@ -1,11 +1,20 @@ package com.faire.ksp.snapshot -import com.google.devtools.ksp.processing.* +import com.faire.ksp.snapshot.handlers.ClassSnapshotHandler +import com.faire.ksp.snapshot.handlers.FunctionSnapshotHandler +import com.faire.ksp.snapshot.handlers.PropertySnapshotHandler +import com.faire.ksp.snapshot.snapshots.DeclarationSnapshot +import com.faire.ksp.snapshot.snapshots.SnapshotResult +import com.google.devtools.ksp.processing.CodeGenerator +import com.google.devtools.ksp.processing.Dependencies +import com.google.devtools.ksp.processing.Resolver +import com.google.devtools.ksp.processing.SymbolProcessor import com.google.devtools.ksp.symbol.KSAnnotated import com.squareup.kotlinpoet.* import java.io.PrintStream +import javax.annotation.processing.Generated -class SnapshotProcessor( +internal class SnapshotProcessor( private val codeGenerator: CodeGenerator, ) : SymbolProcessor { @@ -30,11 +39,14 @@ class SnapshotProcessor( private fun generateSnapshotFile(result: SnapshotResult, writer: SnapshotLiteralWriter) { val objectName = "${result.simpleName}KspSnapshot" + val generatedAnnotation = AnnotationSpec.builder(Generated::class.asClassName()) + .addMember("%S", SnapshotProcessor::class.qualifiedName!!) + .build() val typeSpec = TypeSpec.objectBuilder(objectName) + .addAnnotation(generatedAnnotation) .addModifiers(KModifier.INTERNAL) .addProperty( PropertySpec.builder("snapshot", DeclarationSnapshot::class.asClassName()) - .addModifiers(KModifier.INTERNAL) .initializer(writer.toLiteralCode(result.snapshot)) .build(), ) @@ -42,7 +54,6 @@ class SnapshotProcessor( val fileSpec = FileSpec.builder(result.packageName, objectName) .addType(typeSpec) - .indent(" ".repeat(4)) .build() val outputFile = codeGenerator.createNewFile( diff --git a/test-processor/src/main/kotlin/com/faire/ksp/snapshot/SnapshotProcessorProvider.kt b/test-processor/src/main/kotlin/com/faire/ksp/snapshot/SnapshotProcessorProvider.kt index 1189ff6..8c34aa4 100644 --- a/test-processor/src/main/kotlin/com/faire/ksp/snapshot/SnapshotProcessorProvider.kt +++ b/test-processor/src/main/kotlin/com/faire/ksp/snapshot/SnapshotProcessorProvider.kt @@ -4,7 +4,7 @@ import com.google.devtools.ksp.processing.SymbolProcessor import com.google.devtools.ksp.processing.SymbolProcessorEnvironment import com.google.devtools.ksp.processing.SymbolProcessorProvider -class SnapshotProcessorProvider : SymbolProcessorProvider { +internal class SnapshotProcessorProvider : SymbolProcessorProvider { override fun create(environment: SymbolProcessorEnvironment): SymbolProcessor { return SnapshotProcessor(environment.codeGenerator) } diff --git a/test-processor/src/main/kotlin/com/faire/ksp/snapshot/Snapshots.kt b/test-processor/src/main/kotlin/com/faire/ksp/snapshot/Snapshots.kt deleted file mode 100644 index 81469d5..0000000 --- a/test-processor/src/main/kotlin/com/faire/ksp/snapshot/Snapshots.kt +++ /dev/null @@ -1,37 +0,0 @@ -package com.faire.ksp.snapshot - -sealed interface DeclarationSnapshot - -data class ClassDeclarationSnapshot( - val simpleName: String, - val qualifiedName: String?, - val classKind: String, - val modifiers: List, - val isCompanionObject: Boolean, - val packageName: String, - val origin: String, - val superTypes: List, - val properties: List, - val functions: List, -) : DeclarationSnapshot - -data class PropertyDeclarationSnapshot( - val simpleName: String, - val typeQualifiedName: String?, - val modifiers: List, - val isMutable: Boolean, -) : DeclarationSnapshot - -data class FunctionDeclarationSnapshot( - val simpleName: String, - val returnTypeQualifiedName: String?, - val parameters: List, - val isAbstract: Boolean, -) : DeclarationSnapshot - -data class ValueParameterSnapshot( - val name: String?, - val typeQualifiedName: String?, - val isVararg: Boolean, - val hasDefault: Boolean, -) diff --git a/test-processor/src/main/kotlin/com/faire/ksp/snapshot/handlers/ClassSnapshotHandler.kt b/test-processor/src/main/kotlin/com/faire/ksp/snapshot/handlers/ClassSnapshotHandler.kt new file mode 100644 index 0000000..dc58e6d --- /dev/null +++ b/test-processor/src/main/kotlin/com/faire/ksp/snapshot/handlers/ClassSnapshotHandler.kt @@ -0,0 +1,21 @@ +package com.faire.ksp.snapshot.handlers + +import com.faire.ksp.snapshot.snapshots.SnapshotResult +import com.faire.ksp.snapshot.snapshots.toSnapshot +import com.google.devtools.ksp.symbol.KSAnnotated +import com.google.devtools.ksp.symbol.KSClassDeclaration + +internal class ClassSnapshotHandler : SnapshotHandler { + override fun process(symbol: KSAnnotated): SnapshotResult? { + if (symbol !is KSClassDeclaration) return null + + val qualifiedName = symbol.qualifiedName?.asString() ?: return null + + return SnapshotResult( + qualifiedName = qualifiedName, + simpleName = symbol.simpleName.asString(), + packageName = symbol.packageName.asString(), + snapshot = symbol.toSnapshot(), + ) + } +} \ No newline at end of file diff --git a/test-processor/src/main/kotlin/com/faire/ksp/snapshot/handlers/FunctionSnapshotHandler.kt b/test-processor/src/main/kotlin/com/faire/ksp/snapshot/handlers/FunctionSnapshotHandler.kt new file mode 100644 index 0000000..c158da2 --- /dev/null +++ b/test-processor/src/main/kotlin/com/faire/ksp/snapshot/handlers/FunctionSnapshotHandler.kt @@ -0,0 +1,21 @@ +package com.faire.ksp.snapshot.handlers + +import com.faire.ksp.snapshot.snapshots.SnapshotResult +import com.faire.ksp.snapshot.snapshots.toSnapshot +import com.google.devtools.ksp.symbol.KSAnnotated +import com.google.devtools.ksp.symbol.KSFunctionDeclaration + +internal class FunctionSnapshotHandler : SnapshotHandler { + override fun process(symbol: KSAnnotated): SnapshotResult? { + if (symbol !is KSFunctionDeclaration) return null + + val qualifiedName = symbol.qualifiedName?.asString() ?: return null + + return SnapshotResult( + qualifiedName = qualifiedName, + simpleName = symbol.simpleName.asString(), + packageName = symbol.packageName.asString(), + snapshot = symbol.toSnapshot(), + ) + } +} \ No newline at end of file diff --git a/test-processor/src/main/kotlin/com/faire/ksp/snapshot/handlers/PropertySnapshotHandler.kt b/test-processor/src/main/kotlin/com/faire/ksp/snapshot/handlers/PropertySnapshotHandler.kt new file mode 100644 index 0000000..5593944 --- /dev/null +++ b/test-processor/src/main/kotlin/com/faire/ksp/snapshot/handlers/PropertySnapshotHandler.kt @@ -0,0 +1,21 @@ +package com.faire.ksp.snapshot.handlers + +import com.faire.ksp.snapshot.snapshots.SnapshotResult +import com.faire.ksp.snapshot.snapshots.toSnapshot +import com.google.devtools.ksp.symbol.KSAnnotated +import com.google.devtools.ksp.symbol.KSPropertyDeclaration + +internal class PropertySnapshotHandler : SnapshotHandler { + override fun process(symbol: KSAnnotated): SnapshotResult? { + if (symbol !is KSPropertyDeclaration) return null + + val qualifiedName = symbol.qualifiedName?.asString() ?: return null + + return SnapshotResult( + qualifiedName = qualifiedName, + simpleName = symbol.simpleName.asString(), + packageName = symbol.packageName.asString(), + snapshot = symbol.toSnapshot(), + ) + } +} \ No newline at end of file diff --git a/test-processor/src/main/kotlin/com/faire/ksp/snapshot/handlers/SnapshotHandler.kt b/test-processor/src/main/kotlin/com/faire/ksp/snapshot/handlers/SnapshotHandler.kt new file mode 100644 index 0000000..87105cb --- /dev/null +++ b/test-processor/src/main/kotlin/com/faire/ksp/snapshot/handlers/SnapshotHandler.kt @@ -0,0 +1,9 @@ +package com.faire.ksp.snapshot.handlers + +import com.faire.ksp.snapshot.snapshots.SnapshotResult +import com.google.devtools.ksp.symbol.KSAnnotated + +internal interface SnapshotHandler { + fun process(symbol: KSAnnotated): SnapshotResult? +} + diff --git a/test-processor/src/main/kotlin/com/faire/ksp/snapshot/snapshots/ClassDeclarationSnapshot.kt b/test-processor/src/main/kotlin/com/faire/ksp/snapshot/snapshots/ClassDeclarationSnapshot.kt new file mode 100644 index 0000000..0e7c3d7 --- /dev/null +++ b/test-processor/src/main/kotlin/com/faire/ksp/snapshot/snapshots/ClassDeclarationSnapshot.kt @@ -0,0 +1,15 @@ +package com.faire.ksp.snapshot.snapshots + +data class ClassDeclarationSnapshot( + val simpleName: String, + val qualifiedName: String?, + val classKind: String, + val modifiers: List, + val isCompanionObject: Boolean, + val packageName: String, + val origin: String, + val superTypes: List, + val properties: List, + val functions: List, +) : DeclarationSnapshot + diff --git a/test-processor/src/main/kotlin/com/faire/ksp/snapshot/snapshots/FunctionDeclarationSnapshot.kt b/test-processor/src/main/kotlin/com/faire/ksp/snapshot/snapshots/FunctionDeclarationSnapshot.kt new file mode 100644 index 0000000..40bf026 --- /dev/null +++ b/test-processor/src/main/kotlin/com/faire/ksp/snapshot/snapshots/FunctionDeclarationSnapshot.kt @@ -0,0 +1,8 @@ +package com.faire.ksp.snapshot.snapshots + +data class FunctionDeclarationSnapshot( + val simpleName: String, + val returnTypeQualifiedName: String?, + val parameters: List, + val isAbstract: Boolean, +) : DeclarationSnapshot \ No newline at end of file diff --git a/test-processor/src/main/kotlin/com/faire/ksp/snapshot/KSSnapshotExtensions.kt b/test-processor/src/main/kotlin/com/faire/ksp/snapshot/snapshots/KSSnapshotExtensions.kt similarity index 97% rename from test-processor/src/main/kotlin/com/faire/ksp/snapshot/KSSnapshotExtensions.kt rename to test-processor/src/main/kotlin/com/faire/ksp/snapshot/snapshots/KSSnapshotExtensions.kt index 8961f6d..eb61f5f 100644 --- a/test-processor/src/main/kotlin/com/faire/ksp/snapshot/KSSnapshotExtensions.kt +++ b/test-processor/src/main/kotlin/com/faire/ksp/snapshot/snapshots/KSSnapshotExtensions.kt @@ -1,4 +1,4 @@ -package com.faire.ksp.snapshot +package com.faire.ksp.snapshot.snapshots import com.google.devtools.ksp.symbol.KSClassDeclaration import com.google.devtools.ksp.symbol.KSFunctionDeclaration diff --git a/test-processor/src/main/kotlin/com/faire/ksp/snapshot/snapshots/PropertyDeclarationSnapshot.kt b/test-processor/src/main/kotlin/com/faire/ksp/snapshot/snapshots/PropertyDeclarationSnapshot.kt new file mode 100644 index 0000000..c89f514 --- /dev/null +++ b/test-processor/src/main/kotlin/com/faire/ksp/snapshot/snapshots/PropertyDeclarationSnapshot.kt @@ -0,0 +1,8 @@ +package com.faire.ksp.snapshot.snapshots + +data class PropertyDeclarationSnapshot( + val simpleName: String, + val typeQualifiedName: String?, + val modifiers: List, + val isMutable: Boolean, +) : DeclarationSnapshot \ No newline at end of file diff --git a/test-processor/src/main/kotlin/com/faire/ksp/snapshot/snapshots/SnapshotResult.kt b/test-processor/src/main/kotlin/com/faire/ksp/snapshot/snapshots/SnapshotResult.kt new file mode 100644 index 0000000..869748f --- /dev/null +++ b/test-processor/src/main/kotlin/com/faire/ksp/snapshot/snapshots/SnapshotResult.kt @@ -0,0 +1,8 @@ +package com.faire.ksp.snapshot.snapshots + +internal data class SnapshotResult( + val qualifiedName: String, + val simpleName: String, + val packageName: String, + val snapshot: DeclarationSnapshot, +) \ No newline at end of file diff --git a/test-processor/src/main/kotlin/com/faire/ksp/snapshot/snapshots/Snapshots.kt b/test-processor/src/main/kotlin/com/faire/ksp/snapshot/snapshots/Snapshots.kt new file mode 100644 index 0000000..0e8284c --- /dev/null +++ b/test-processor/src/main/kotlin/com/faire/ksp/snapshot/snapshots/Snapshots.kt @@ -0,0 +1,4 @@ +package com.faire.ksp.snapshot.snapshots + +sealed interface DeclarationSnapshot + diff --git a/test-processor/src/main/kotlin/com/faire/ksp/snapshot/snapshots/ValueParameterSnapshot.kt b/test-processor/src/main/kotlin/com/faire/ksp/snapshot/snapshots/ValueParameterSnapshot.kt new file mode 100644 index 0000000..3c91d40 --- /dev/null +++ b/test-processor/src/main/kotlin/com/faire/ksp/snapshot/snapshots/ValueParameterSnapshot.kt @@ -0,0 +1,8 @@ +package com.faire.ksp.snapshot.snapshots + +data class ValueParameterSnapshot( + val name: String?, + val typeQualifiedName: String?, + val isVararg: Boolean, + val hasDefault: Boolean, +) \ No newline at end of file