Initial commit

This commit is contained in:
2025-10-07 07:39:24 +08:00
commit afa0ee5c2e
74 changed files with 3140 additions and 0 deletions

View File

@@ -0,0 +1,35 @@
plugins {
alias(libs.plugins.kotlin.jvm)
alias(libs.plugins.kotlin.ksp)
alias(libs.plugins.maven.publish)
}
group = property.project.groupName
version = property.project.version
java {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
kotlin {
jvmToolchain(17)
compilerOptions {
freeCompilerArgs = listOf(
"-Xno-param-assertions",
"-Xno-call-assertions",
"-Xno-receiver-assertions"
)
}
}
dependencies {
compileOnly(libs.ksp.api)
ksp(libs.auto.service.ksp)
implementation(libs.moshi.kotlin)
implementation(libs.auto.service.annotations)
implementation(libs.kotlinpoet)
implementation(libs.kotlinpoet.ksp)
}

View File

@@ -0,0 +1,48 @@
/*
* Moshi Companion - Companion to Moshi with more practical features.
* Copyright (C) 2019 HighCapable
* https://github.com/HighCapable/moshi-companion
*
* Apache License Version 2.0
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* This file is created by fankes on 2025/10/2.
*/
@file:Suppress("unused", "MemberVisibilityCanBePrivate")
package com.highcapable.moshi.companion.codegen
object DeclaredSymbol {
const val MOSHI_PACKAGE_NAME = "com.squareup.moshi"
const val MOSHI_CLASS_NAME = "Moshi"
const val MOSHI_CLASS = "$MOSHI_PACKAGE_NAME.$MOSHI_CLASS_NAME"
const val JSON_ANNOTATION_CLASS_NAME = "JsonClass"
const val JSON_ANNOTATION_CLASS = "$MOSHI_PACKAGE_NAME.$JSON_ANNOTATION_CLASS_NAME"
const val JSON_ADAPTER_CLASS_NAME = "JsonAdapter"
const val JSON_ADAPTER_CLASS = "$MOSHI_PACKAGE_NAME.$JSON_ADAPTER_CLASS_NAME"
const val MOSHI_COMPANION_API_PACKAGE_NAME = "com.highcapable.moshi.companion.api"
const val MOSHI_COMPANION_CLASS_NAME = "MoshiCompanion"
const val MOSHI_COMPANION_CLASS = "$MOSHI_COMPANION_API_PACKAGE_NAME.$MOSHI_COMPANION_CLASS_NAME"
const val TYPE_REF_CLASS_NAME = "TypeRef"
const val TYPE_REF_CLASS = "$MOSHI_COMPANION_API_PACKAGE_NAME.$TYPE_REF_CLASS_NAME"
const val ADAPTER_REGISTRY_CLASS_NAME = "AdapterRegistry"
}

View File

@@ -0,0 +1,51 @@
/*
* Moshi Companion - Companion to Moshi with more practical features.
* Copyright (C) 2019 HighCapable
* https://github.com/HighCapable/moshi-companion
*
* Apache License Version 2.0
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* This file is created by fankes on 2025/10/2.
*/
@file:Suppress("unused")
package com.highcapable.moshi.companion.codegen
import com.google.auto.service.AutoService
import com.google.devtools.ksp.processing.Resolver
import com.google.devtools.ksp.processing.SymbolProcessor
import com.google.devtools.ksp.processing.SymbolProcessorEnvironment
import com.google.devtools.ksp.processing.SymbolProcessorProvider
import com.google.devtools.ksp.symbol.KSAnnotated
import com.highcapable.moshi.companion.codegen.subprocessor.AdapterRegistryGenerator
@AutoService(SymbolProcessorProvider::class)
class MoshiCompanionProcessor : SymbolProcessorProvider {
override fun create(environment: SymbolProcessorEnvironment) = object : SymbolProcessor {
private val subProcessor = listOf(
AdapterRegistryGenerator(environment)
)
override fun process(resolver: Resolver) = emptyList<KSAnnotated>().let { startProcess(resolver); it }
private fun startProcess(resolver: Resolver) {
subProcessor.forEach {
it.startProcess(resolver)
}
}
}
}

View File

@@ -0,0 +1,64 @@
/*
* Moshi Companion - Companion to Moshi with more practical features.
* Copyright (C) 2019 HighCapable
* https://github.com/HighCapable/moshi-companion
*
* Apache License Version 2.0
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* This file is created by fankes on 2025/10/3.
*/
package com.highcapable.moshi.companion.codegen
object Options {
/**
* This boolean processing option is used to read the parameters of the Moshi
* ontology project "moshi-kotlin-codegen", which should be configured by the user as "false".
*/
const val OPTION_GENERATE_PROGUARD_RULES_MOSHI = "moshi.generateProguardRules"
/**
* This string processing option can change the package name of the generated AdapterRegistry implementation class.
* Default is `com.highcapable.moshi.companion.generated` + no repetition package name hash.
*/
const val OPTION_GENERATE_ADAPTER_REGISTRY_PACKAGE_NAME = "moshi-companion.generateAdapterRegistryPackageName"
/**
* This string processing option can change the class name of the generated AdapterRegistry implementation class.
* Default is `DefaultMoshiAdapterRegistry`.
*/
const val OPTION_GENERATE_ADAPTER_REGISTRY_CLASS_NAME = "moshi-companion.generateAdapterRegistryClassName"
/**
* This boolean processing option can change the access modifier of the generated AdapterRegistry implementation class.
* If true, the class will have internal access; if false, it will have public access.
* This is disabled by default.
*/
const val OPTION_GENERATE_ADAPTER_REGISTRY_RESTRICTED_ACCESS = "moshi-companion.generateAdapterRegistryRestrictedAccess"
/**
* This boolean processing option can disable proguard rule generation.
* Normally, this is not recommended unless end-users build their own JsonAdapter look-up tool.
* This is enabled by default.
*/
const val OPTION_GENERATE_PROGUARD_RULES = "moshi-companion.generateProguardRules"
/**
* This boolean processing option can enable keeping enum classes values method in proguard rules.
* Normally, this is not recommended unless you are sure that you don't need enum values method.
* This is enabled by default.
*/
const val OPTION_PROGUARD_RULES_KEEP_ENUM_CLASSES = "moshi-companion.proguardRulesKeepEnumClasses"
}

View File

@@ -0,0 +1,40 @@
/*
* Moshi Companion - Companion to Moshi with more practical features.
* Copyright (C) 2019 HighCapable
* https://github.com/HighCapable/moshi-companion
*
* Apache License Version 2.0
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* This file is created by fankes on 2025/10/2.
*/
package com.highcapable.moshi.companion.codegen.extension
import com.google.devtools.ksp.KspExperimental
import com.google.devtools.ksp.getAnnotationsByType
import com.google.devtools.ksp.symbol.KSAnnotated
import java.security.MessageDigest
@OptIn(KspExperimental::class)
inline fun <reified T : Annotation> KSAnnotated.findAnnotationWithType() = getAnnotationsByType(T::class).firstOrNull()
object HashString {
private val md = MessageDigest.getInstance("SHA-256")
fun generate(input: String, length: Int = 16): String {
val digest = md.digest(input.toByteArray())
return digest.joinToString("") { "%02x".format(it) }.take(length)
}
}

View File

@@ -0,0 +1,326 @@
/*
* Moshi Companion - Companion to Moshi with more practical features.
* Copyright (C) 2019 HighCapable
* https://github.com/HighCapable/moshi-companion
*
* Apache License Version 2.0
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* This file is created by fankes on 2025/10/2.
*/
package com.highcapable.moshi.companion.codegen.subprocessor
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.SymbolProcessorEnvironment
import com.google.devtools.ksp.symbol.KSClassDeclaration
import com.google.devtools.ksp.symbol.KSDeclaration
import com.google.devtools.ksp.symbol.KSFile
import com.highcapable.moshi.companion.codegen.DeclaredSymbol
import com.highcapable.moshi.companion.codegen.Options
import com.highcapable.moshi.companion.codegen.extension.HashString
import com.highcapable.moshi.companion.codegen.extension.findAnnotationWithType
import com.highcapable.moshi.companion.codegen.subprocessor.base.BaseSymbolProcessor
import com.squareup.kotlinpoet.ClassName
import com.squareup.kotlinpoet.CodeBlock
import com.squareup.kotlinpoet.FileSpec
import com.squareup.kotlinpoet.KModifier
import com.squareup.kotlinpoet.ParameterizedTypeName.Companion.parameterizedBy
import com.squareup.kotlinpoet.PropertySpec
import com.squareup.kotlinpoet.STAR
import com.squareup.kotlinpoet.TypeSpec
import com.squareup.kotlinpoet.WildcardTypeName
import com.squareup.kotlinpoet.asClassName
import com.squareup.kotlinpoet.ksp.toClassName
import com.squareup.kotlinpoet.ksp.writeTo
import com.squareup.moshi.JsonClass
import java.io.OutputStreamWriter
import java.nio.charset.StandardCharsets
class AdapterRegistryGenerator(override val environment: SymbolProcessorEnvironment) : BaseSymbolProcessor(environment) {
private companion object {
private const val ADAPTER_REGISTRY_PACKAGE_NAME_SUFFIX = "generated"
private const val DEFAULT_ADAPTER_REGISTRY_PACKAGE_NAME_PREFIX = "com.highcapable.moshi.companion"
private const val DEFAULT_ADAPTER_REGISTRY_CLASS_NAME = "DefaultMoshiAdapterRegistry"
val JsonAdapterClass = ClassName(DeclaredSymbol.MOSHI_PACKAGE_NAME, DeclaredSymbol.JSON_ADAPTER_CLASS_NAME)
val AdapterRegistryClass = ClassName(DeclaredSymbol.MOSHI_COMPANION_API_PACKAGE_NAME, DeclaredSymbol.MOSHI_COMPANION_CLASS_NAME)
.nestedClass(DeclaredSymbol.ADAPTER_REGISTRY_CLASS_NAME)
val TypeClass = ClassName("java.lang.reflect", "Type")
}
private val generateAdapterRegistryPackageName = environment.options[Options.OPTION_GENERATE_ADAPTER_REGISTRY_PACKAGE_NAME]
private val generateAdapterRegistryClassName = environment.options[Options.OPTION_GENERATE_ADAPTER_REGISTRY_CLASS_NAME]
private val generateAdapterRegistryRestrictedAccess = environment.options[Options.OPTION_GENERATE_ADAPTER_REGISTRY_RESTRICTED_ACCESS]
?.toBooleanStrictOrNull() ?: false
private val generateProguardRulesMoshi = environment.options[Options.OPTION_GENERATE_PROGUARD_RULES_MOSHI]?.toBooleanStrictOrNull()
private val generateProguardRules = environment.options[Options.OPTION_GENERATE_PROGUARD_RULES]?.toBooleanStrictOrNull() ?: true
private val keepEnumClasses = environment.options[Options.OPTION_PROGUARD_RULES_KEEP_ENUM_CLASSES]?.toBooleanStrictOrNull() ?: true
private val adapterRegistrations = mutableListOf<AdapterRegistration>()
private var originatingFile: KSFile? = null
override fun startProcess(resolver: Resolver) {
processRegistrations(resolver)
generateCodeFile()
generateProguardFile()
}
private fun processRegistrations(resolver: Resolver) {
resolver.getSymbolsWithAnnotation(JsonClass::class.qualifiedName!!).forEach { type ->
// For the smart cast.
if (type !is KSDeclaration) return@forEach
// Use the first obtained source file as the dependency of the generated file.
if (originatingFile == null)
originatingFile = type.containingFile ?: return@forEach
val jsonClassAnnotation = type.findAnnotationWithType<JsonClass>() ?: return@forEach
if (!jsonClassAnnotation.generateAdapter) return@forEach
runCatching {
// Collect adapter registration information.
if (type is KSClassDeclaration) {
val targetClassName = type.toClassName()
val adapterClassName = ClassName(
targetClassName.packageName,
"${targetClassName.simpleNames.joinToString("_")}JsonAdapter"
)
val hasTypeParameters = type.typeParameters.isNotEmpty()
adapterRegistrations += AdapterRegistration(
targetClass = targetClassName,
adapterClass = adapterClassName,
hasTypeParameters = hasTypeParameters
)
}
}.onFailure {
logger.error("Error preparing ${type.simpleName.asString()}\n${it.stackTraceToString()}")
}
}
}
private fun generateCodeFile() {
if (adapterRegistrations.isNotEmpty()) {
val fileSpec = generateAdapterRegistry()
try {
fileSpec.writeTo(codeGenerator, aggregating = true)
} catch (_: FileAlreadyExistsException) {
// Ignored, FileAlreadyExistsException can happen if multiple compilations.
}
}
}
private fun generateProguardFile() {
if (generateProguardRules) require(generateProguardRulesMoshi == false) {
"Proguard rules generation for Moshi is enabled. " +
"Please disable it by setting the \"${Options.OPTION_GENERATE_PROGUARD_RULES_MOSHI}\" option to \"false\" " +
"in your build configuration to avoid duplicate rules."
}
if (generateProguardRules) {
val config = ProguardConfig(adapterRegistrations, keepEnumClasses)
try {
config.writeTo(codeGenerator)
} catch (_: FileAlreadyExistsException) {
// Ignored, FileAlreadyExistsException can happen if multiple compilations.
}
}
}
private fun generateAdapterRegistry(): FileSpec {
// Get the top-level package name of the project.
val packageName = run {
adapterRegistrations
.filterByPackageNameFirstOrNull()
?.targetClass?.packageName
} ?: "default" // fallback package name
val registryPackageName = (generateAdapterRegistryPackageName ?: run {
val packageHash = HashString.generate(packageName)
"$DEFAULT_ADAPTER_REGISTRY_PACKAGE_NAME_PREFIX.r$packageHash"
}) + ".$ADAPTER_REGISTRY_PACKAGE_NAME_SUFFIX"
val adaptersMapBuilder = CodeBlock.builder()
adaptersMapBuilder.add("\nmapOf(")
adapterRegistrations.forEachIndexed { index, registration ->
if (index > 0) adaptersMapBuilder.add(",")
adaptersMapBuilder.add(
"\n %T::class.java to %T::class.java",
registration.targetClass,
registration.adapterClass
)
}
if (adapterRegistrations.isNotEmpty())
adaptersMapBuilder.add("\n")
adaptersMapBuilder.add(")")
val adaptersProperty = PropertySpec.builder(
name = "adapters",
Map::class.asClassName().parameterizedBy(
TypeClass, Class::class.asClassName().parameterizedBy(
WildcardTypeName.producerOf(JsonAdapterClass.parameterizedBy(STAR))
)
)
).apply {
addModifiers(KModifier.OVERRIDE)
initializer(adaptersMapBuilder.build())
}.build()
val adapterRegistryClassName = generateAdapterRegistryClassName ?: DEFAULT_ADAPTER_REGISTRY_CLASS_NAME
val registryClass = TypeSpec.classBuilder(adapterRegistryClassName).apply {
if (generateAdapterRegistryRestrictedAccess)
addModifiers(KModifier.INTERNAL)
else addModifiers(KModifier.PUBLIC)
addSuperinterface(AdapterRegistryClass)
addProperty(adaptersProperty)
}.build()
val fileSpec = FileSpec.builder(registryPackageName, adapterRegistryClassName).apply {
addFileComment(
"""
This file is auto generated by Moshi Companion CodeGen.
**DO NOT EDIT THIS FILE MANUALLY**
""".trimIndent()
)
addType(registryClass)
}.build()
return fileSpec
}
private fun List<AdapterRegistration>.filterByPackageNameFirstOrNull(): AdapterRegistration? {
val registrations = filter { it.targetClass.packageName.isNotEmpty() }
if (registrations.isEmpty()) return null
// Count the number of prefixes for each class name.
val prefixCount = mutableMapOf<String, Int>()
val splitMap = registrations.associateWith { it.targetClass.packageName.split(".") }
splitMap.values.forEach { parts ->
val prefix = parts.take(3).joinToString(".")
prefixCount[prefix] = (prefixCount[prefix] ?: 0) + 1
}
// Find the prefix with the least number of occurrences.
val oddPrefix = prefixCount.minByOrNull { it.value }?.key
// Check whether there are different class names.
val oddClassName = splitMap.filter { it.value.take(3).joinToString(".") == oddPrefix }.keys.firstOrNull()
if (oddClassName != null && prefixCount[oddPrefix] == 1) return oddClassName
// If there is no different style, return the shortest class name.
return registrations.minByOrNull { it.targetClass.packageName.length }
}
private fun ProguardConfig.writeTo(codeGenerator: CodeGenerator) {
val originatingFile = originatingFile ?: return
val file = codeGenerator.createNewFile(
dependencies = Dependencies(aggregating = true, originatingFile),
packageName = "",
fileName = outputFilePathWithoutExtension,
extensionName = "pro"
)
OutputStreamWriter(file, StandardCharsets.UTF_8).use(::writeTo)
}
private data class AdapterRegistration(
val targetClass: ClassName,
val adapterClass: ClassName,
val hasTypeParameters: Boolean
)
private data class ProguardConfig(
private val registrations: List<AdapterRegistration>,
private val keepEnumClasses: Boolean
) {
private companion object {
const val DEFAULT_OUTPUT_FILE_PATH = "META-INF/proguard/moshi-companion"
}
val outputFilePathWithoutExtension
get() = "$DEFAULT_OUTPUT_FILE_PATH-r${HashString.generate(registrations.first().targetClass.canonicalName)}"
fun writeTo(out: Appendable) = out.run {
// Keep the `DefaultConstructorMarker` class, needed for synthetic constructors with default parameters.
appendLine("-keepnames class kotlin.jvm.internal.DefaultConstructorMarker")
appendLine()
// Keep the `JsonClass` annotation on the target class.
appendLine("-keep,allowobfuscation @${DeclaredSymbol.JSON_ANNOTATION_CLASS} class *")
appendLine()
// Keep the `TypeRef` class and its subclasses.
appendLine("-keep,allowobfuscation class ${DeclaredSymbol.TYPE_REF_CLASS} {")
appendLine(" <fields>;")
appendLine(" <methods>;")
appendLine("}")
appendLine()
appendLine("-keep,allowobfuscation class * extends ${DeclaredSymbol.TYPE_REF_CLASS}")
appendLine()
// Keep generic signatures.
appendLine("-keepattributes Signature")
appendLine()
// Keep the enum values method.
if (keepEnumClasses) {
appendLine("-keepclassmembers enum * {")
appendLine(" public static **[] values();")
appendLine(" public static ** valueOf(java.lang.String);")
appendLine(" public static <fields>;")
appendLine("}")
appendLine()
}
registrations.forEach { registration ->
val targetName = registration.targetClass.reflectionName()
val adapterCanonicalName = ClassName(
registration.targetClass.packageName,
registration.adapterClass.simpleName
).canonicalName
// Keep the constructor for Moshi's reflective lookup.
appendLine("-if class $targetName")
appendLine("-keepclassmembers class $adapterCanonicalName {")
if (registration.hasTypeParameters)
appendLine(" public <init>(${DeclaredSymbol.MOSHI_CLASS}, ${TypeClass.canonicalName}[]);")
else appendLine(" public <init>(${DeclaredSymbol.MOSHI_CLASS});")
appendLine("}")
appendLine()
// Keep the synthetic constructor if the target class has default parameter values.
appendLine("-keepclassmembers class $targetName {")
appendLine(" public synthetic <init>(...);")
appendLine("}")
appendLine()
}
}
}
}

View File

@@ -0,0 +1,33 @@
/*
* Moshi Companion - Companion to Moshi with more practical features.
* Copyright (C) 2019 HighCapable
* https://github.com/HighCapable/moshi-companion
*
* Apache License Version 2.0
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* This file is created by fankes on 2025/10/2.
*/
package com.highcapable.moshi.companion.codegen.subprocessor.base
import com.google.devtools.ksp.processing.Resolver
import com.google.devtools.ksp.processing.SymbolProcessorEnvironment
abstract class BaseSymbolProcessor(protected open val environment: SymbolProcessorEnvironment) {
protected val logger by lazy { environment.logger }
protected val codeGenerator by lazy { environment.codeGenerator }
abstract fun startProcess(resolver: Resolver)
}