Skip to content
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
28 changes: 28 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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)
}
}
Original file line number Diff line number Diff line change
@@ -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,
)
Original file line number Diff line number Diff line change
@@ -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
}
Original file line number Diff line number Diff line change
@@ -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(")")
}
}
}
Original file line number Diff line number Diff line change
@@ -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 {

Expand All @@ -30,19 +39,21 @@ 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(),
)
.build()

val fileSpec = FileSpec.builder(result.packageName, objectName)
.addType(typeSpec)
.indent(" ".repeat(4))
.build()

val outputFile = codeGenerator.createNewFile(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down
37 changes: 0 additions & 37 deletions test-processor/src/main/kotlin/com/faire/ksp/snapshot/Snapshots.kt

This file was deleted.

Original file line number Diff line number Diff line change
@@ -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(),
)
}
}
Original file line number Diff line number Diff line change
@@ -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(),
)
}
}
Original file line number Diff line number Diff line change
@@ -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(),
)
}
}
Original file line number Diff line number Diff line change
@@ -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?
}

Original file line number Diff line number Diff line change
@@ -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<String>,
val isCompanionObject: Boolean,
val packageName: String,
val origin: String,
val superTypes: List<String>,
val properties: List<PropertyDeclarationSnapshot>,
val functions: List<FunctionDeclarationSnapshot>,
) : DeclarationSnapshot

Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.faire.ksp.snapshot.snapshots

data class FunctionDeclarationSnapshot(
val simpleName: String,
val returnTypeQualifiedName: String?,
val parameters: List<ValueParameterSnapshot>,
val isAbstract: Boolean,
) : DeclarationSnapshot
Original file line number Diff line number Diff line change
@@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.faire.ksp.snapshot.snapshots

data class PropertyDeclarationSnapshot(
val simpleName: String,
val typeQualifiedName: String?,
val modifiers: List<String>,
val isMutable: Boolean,
) : DeclarationSnapshot
Original file line number Diff line number Diff line change
@@ -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,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
package com.faire.ksp.snapshot.snapshots

sealed interface DeclarationSnapshot

Original file line number Diff line number Diff line change
@@ -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,
)