com.flyjingfish.module_communication_ksp.CommunicationKspSymbolProcessor.kt Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of module-communication-ksp Show documentation
Show all versions of module-communication-ksp Show documentation
Lightweight Aop for Android platform, you deserve it, action is worse than your heartbeat
package com.flyjingfish.module_communication_ksp
import com.flyjingfish.module_communication_annotation.BaseRouter
import com.flyjingfish.module_communication_annotation.BaseRouterClass
import com.flyjingfish.module_communication_annotation.BindClass
import com.flyjingfish.module_communication_annotation.ExposeBean
import com.flyjingfish.module_communication_annotation.ExposeInterface
import com.flyjingfish.module_communication_annotation.ImplementClass
import com.flyjingfish.module_communication_annotation.Route
import com.flyjingfish.module_communication_annotation.RouteParams
import com.google.devtools.ksp.containingFile
import com.google.devtools.ksp.getAllSuperTypes
import com.google.devtools.ksp.processing.CodeGenerator
import com.google.devtools.ksp.processing.Dependencies
import com.google.devtools.ksp.processing.KSPLogger
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.symbol.FileLocation
import com.google.devtools.ksp.symbol.KSAnnotated
import com.google.devtools.ksp.symbol.KSClassDeclaration
import com.google.devtools.ksp.symbol.KSFile
import com.google.devtools.ksp.symbol.KSPropertyDeclaration
import com.google.devtools.ksp.symbol.KSType
import com.google.devtools.ksp.validate
import com.squareup.kotlinpoet.ClassName
import com.squareup.kotlinpoet.FileSpec
import com.squareup.kotlinpoet.FunSpec
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 java.io.File
import java.io.FileInputStream
import java.util.Locale
class CommunicationKspSymbolProcessor(
private val environment: SymbolProcessorEnvironment,
) : SymbolProcessor {
private val codeGenerator: CodeGenerator = environment.codeGenerator
private val logger: KSPLogger = environment.logger
private lateinit var moduleName :String
override fun process(resolver: Resolver): List {
val symbols =
resolver.getSymbolsWithAnnotation(ImplementClass::class.qualifiedName!!)
val ret1 = processImplementClass(symbols)
val ret3 = processExposeInterface(resolver,symbols)
val ret2 = processExposeBean(resolver)
val ret4 = processRouteParams(resolver)
val ret5 = processRoute(resolver)
val ret = arrayListOf()
ret.addAll(ret1)
ret.addAll(ret2)
ret.addAll(ret3)
ret.addAll(ret4)
ret.addAll(ret5)
return ret
}
private fun processImplementClass(symbols: Sequence): List {
for (symbol in symbols) {
val annotationMap = getAnnotation(symbol)
val classMethodMap: MutableMap =
annotationMap["@ImplementClass"] ?: continue
val value: KSType =
(if (classMethodMap["value"] != null) classMethodMap["value"] as KSType else null)
?: continue
val targetClassName: String =
(value.declaration.packageName.asString() + "." + value.toString())
isImplementClass(symbol,targetClassName)
val implementClassName = ClassName((symbol as KSClassDeclaration).packageName.asString(), "$symbol")
val bindClassName = ClassName.bestGuess(BindClass::class.qualifiedName!!)
val superinterface = bindClassName.parameterizedBy(implementClassName)
val className = symbol.packageName.asString() + "." + symbol
val fileName = "${value.toString()}\$\$BindClass";
val typeBuilder = TypeSpec.classBuilder(
fileName
).addModifiers(KModifier.FINAL).addSuperinterface(superinterface)
val whatsMyName1 = whatsMyName("getImplementClassInstance")
.addModifiers(KModifier.OVERRIDE)
.addModifiers(KModifier.FINAL)
.addModifiers(KModifier.PUBLIC)
.returns(ClassName.bestGuess(className))
.addStatement("return $symbol()")
typeBuilder.addFunction(whatsMyName1.build())
writeToFile(typeBuilder, value.declaration.packageName.asString(),fileName, symbol)
}
return symbols.filter { !it.validate() }.toList()
}
private fun processExposeBean(resolver: Resolver): List {
val symbols =
resolver.getSymbolsWithAnnotation(ExposeBean::class.qualifiedName!!)
for (symbol in symbols) {
if (symbol is KSClassDeclaration) {
val file = File((symbol.location as FileLocation).filePath)
val fileName =
"${symbol}${file.absolutePath.substring(file.absolutePath.lastIndexOf("."))}";
writeToFile(fileName, symbol, symbol.packageName.asString(), file)
}
}
return symbols.filter { !it.validate() }.toList()
}
private fun processRoute(resolver: Resolver): List {
val symbols =
resolver.getSymbolsWithAnnotation(Route::class.qualifiedName!!).toList()
if (symbols.isNotEmpty()){
val routeModuleName = environment.options["routeModuleName"]
?: throw IllegalArgumentException("注意:你还没有给当前 module 设置 communication.export 插件")
val routeModulePackageName = environment.options["routeModulePackageName"]
?: throw IllegalArgumentException("注意:你还没有给当前 module 设置 communication.export 插件")
val exportEmptyRoute = environment.options["exportEmptyRoute"]
?: "false"
val emptyRoute = exportEmptyRoute == "true"
var fullName = ""
val names = routeModuleName.split("-");
for(token in names){
if ("" != token){
fullName += (token.replaceFirstChar {
if (it.isLowerCase()) it.titlecase(
Locale.getDefault()
) else it.toString()
})
}
}
moduleName = fullName
val routeClassFile = "$moduleName\$\$RouterClass"
val classBuilder = TypeSpec.objectBuilder(
routeClassFile
).addSuperinterface(ClassName.bestGuess(BaseRouterClass::class.qualifiedName!!))
val routeFile = "$moduleName\$\$Router"
val routeBuilder = TypeSpec.objectBuilder(
routeFile
).addSuperinterface(ClassName.bestGuess(BaseRouter::class.qualifiedName!!))
val ksFiles = mutableListOf()
for (symbol in symbols) {
val annotationMap = getAnnotation(symbol)
val className = (symbol as KSClassDeclaration).packageName.asString() + "." + symbol
val path : String = annotationMap["@Route"]?.get("path") as String
val classKey:String
val routeClassName = if (path.isNotEmpty()){
classKey = path.replace(".","_").replace("/","_")
.replaceFirstChar { if (it.isLowerCase()) it.titlecase(Locale.ROOT) else it.toString() }
classKey
}else{
classKey = className.replace(".","_")
"$symbol"
}
val bindClassName = ClassName.bestGuess(Class::class.qualifiedName!!)
val returnType = bindClassName.parameterizedBy(STAR)
// val android = PropertySpec.builder(classKey, returnType)
// .initializer("%T::class.java", ClassName.bestGuess(
// className
// ))
// .build()
// val android = PropertySpec.builder(classKey, returnType.copy(nullable = true))
// .mutable()
// .initializer("null")
// .addModifiers(KModifier.PRIVATE)
// .build()
// classBuilder.addProperty(android)
val paramMap = routeParamsMap[className]
val classFunName = "get${classKey}Class"
if (isSubtype(symbol,"android.app.Activity")){
val whatsMyName1 = whatsMyName("go$routeClassName")
if (!emptyRoute){
classBuilder.addFunction(whatsMyName(classFunName)
.returns(returnType.copy(nullable = true))
.addStatement("return $className::class.java")
.build())
val activityBuilder = TypeSpec.classBuilder(
symbol.toString()
).superclass(ClassName.bestGuess("android.app.Activity"))
writeToFile(activityBuilder, symbol.packageName.asString(),symbol.toString(),symbol,true,false)
whatsMyName1.addParameter("context",ClassName.bestGuess(
"android.content.Context"
))
whatsMyName1.addStatement(
"val intent = %T(context,`$routeClassFile`.$classFunName())",
ClassName.bestGuess(
"android.content.Intent"
)
)
paramMap?.forEach { (_, value) ->
val config = value.annoMap["@RouteParams"]
if (config != null){
val paramsName : String = config["name"] as String
val paramNullable : Boolean = config["nullable"] as Boolean
val targetClassName: String = value.className
whatsMyName1.addParameter(paramsName,ClassName.bestGuess(targetClassName).copy(nullable = paramNullable))
whatsMyName1.addStatement(
"intent.putExtra(\"$paramsName\",$paramsName)",
)
}
}
whatsMyName1.addStatement(
"context.startActivity(intent)",
)
}else{
classBuilder.addFunction(whatsMyName(classFunName)
.returns(returnType.copy(nullable = true))
.addStatement("return null")
.build())
whatsMyName1.addParameter("context",ClassName.bestGuess(
"android.content.Context"
))
paramMap?.forEach { (_, value) ->
val config = value.annoMap["@RouteParams"]
if (config != null){
val paramsName : String = config["name"] as String
val paramNullable : Boolean = config["nullable"] as Boolean
val targetClassName: String = value.className
whatsMyName1.addParameter(paramsName,ClassName.bestGuess(targetClassName).copy(nullable = paramNullable))
}
}
}
routeBuilder.addFunction(whatsMyName1.build())
}else if (isSubtype(symbol,"androidx.fragment.app.Fragment") || isSubtype(symbol,"android.app.Fragment")){
val anyClassName = ClassName.bestGuess(Any::class.qualifiedName!!)
val whatsMyName2 = whatsMyName("new$routeClassName")
.returns(anyClassName.copy(nullable = true))
if (!emptyRoute){
classBuilder.addFunction(whatsMyName(classFunName)
.returns(anyClassName.copy(nullable = true))
.addStatement("return $className()")
.build())
val activityBuilder = TypeSpec.classBuilder(
symbol.toString()
)
if (isSubtype(symbol,"androidx.fragment.app.Fragment")){
activityBuilder.superclass(ClassName.bestGuess("androidx.fragment.app.Fragment"))
}else{
activityBuilder.superclass(ClassName.bestGuess("android.app.Fragment"))
}
writeToFile(activityBuilder, symbol.packageName.asString(),symbol.toString(),symbol,true,false)
whatsMyName2.addStatement(
"val intent = %T()",
ClassName.bestGuess(
"android.content.Intent"
)
)
whatsMyName2.addStatement(
"val instance : %T = `$routeClassFile`.$classFunName()",
anyClassName.copy(nullable = true)
)
paramMap?.forEach { (_, value) ->
val config = value.annoMap["@RouteParams"]
if (config != null){
val paramsName : String = config["name"] as String
val paramNullable : Boolean = config["nullable"] as Boolean
val targetClassName: String = value.className
whatsMyName2.addParameter(paramsName,ClassName.bestGuess(targetClassName).copy(nullable = paramNullable))
whatsMyName2.addStatement(
"intent.putExtra(\"$paramsName\",$paramsName)",
)
}
}
if (isSubtype(symbol,"androidx.fragment.app.Fragment")){
whatsMyName2.addStatement(
"if (instance is %T) {"
,ClassName.bestGuess(
"androidx.fragment.app.Fragment"
)
)
}else{
whatsMyName2.addStatement(
"if (instance is %T) {"
,ClassName.bestGuess(
"android.app.Fragment"
)
)
}
whatsMyName2.addStatement(
" instance.arguments = intent.getExtras()"
)
whatsMyName2.addStatement(
"}"
)
whatsMyName2.addStatement(
"return instance"
)
}else{
classBuilder.addFunction(whatsMyName(classFunName)
.returns(anyClassName.copy(nullable = true))
.addStatement("return null")
.build())
paramMap?.forEach { (_, value) ->
val config = value.annoMap["@RouteParams"]
if (config != null){
val paramsName : String = config["name"] as String
val paramNullable : Boolean = config["nullable"] as Boolean
val targetClassName: String = value.className
whatsMyName2.addParameter(paramsName,ClassName.bestGuess(targetClassName).copy(nullable = paramNullable))
}
}
whatsMyName2.addStatement(
"return null"
)
}
routeBuilder.addFunction(whatsMyName2.build())
}
ksFiles.add(symbol.containingFile!!)
}
writeToFile(classBuilder, routeModulePackageName,routeClassFile,true, ksFiles.toTypedArray())
writeToFile(routeBuilder, routeModulePackageName,routeFile, true,ksFiles.toTypedArray())
}
return symbols.filter { !it.validate() }.toList()
}
private fun isSubtype(symbol: KSClassDeclaration,superType :String):Boolean{
symbol.getAllSuperTypes().toList().forEach {
val className = "${it.declaration.packageName.asString()}.${it}"
if (className == superType){
return true
}
// logger.error("symbol=${symbol},superTypes= ${it.declaration.packageName.asString()+"."+it}")
}
return false
}
private val routeParamsMap = mutableMapOf>()
private fun processRouteParams(resolver: Resolver): List {
val symbols =
resolver.getSymbolsWithAnnotation(RouteParams::class.qualifiedName!!)
for (symbol in symbols) {
val annotationMap = getAnnotation(symbol)
var className = "${(symbol as KSPropertyDeclaration).packageName.asString()}."
var parent = symbol.parent
while (parent !is KSFile){
className = "$className$parent."
parent = parent?.parent
}
className = className.substring(0,className.length-1)
val config = RouteParamsConfig("${symbol.type.resolve().declaration.packageName.asString()}.${symbol.type}",annotationMap)
var map = routeParamsMap[className]
if (map == null){
map = mutableMapOf()
routeParamsMap[className] = map
}
val key = annotationMap["@RouteParams"].toString()
map[key] = config
// logger.error("annotationMap=$annotationMap")
// logger.error("symbolLocation=$className${symbol}")
// logger.error("symbolType=${symbol.type.resolve().declaration.packageName.asString()}.${symbol.type}")
}
return symbols.filter { !it.validate() }.toList()
}
private fun processExposeInterface(resolver: Resolver,implementSymbols: Sequence): List {
val symbols =
resolver.getSymbolsWithAnnotation(ExposeInterface::class.qualifiedName!!)
for (symbol in symbols) {
if (symbol is KSClassDeclaration) {
val className = symbol.packageName.asString() + "." + symbol
val isContain = isContainImplementClass(implementSymbols,className)
if (!isContain){
throw IllegalArgumentException("注意: $className 没有相应的实现类")
}
val file = File((symbol.location as FileLocation).filePath)
val fileName =
"${symbol}${file.absolutePath.substring(file.absolutePath.lastIndexOf("."))}";
writeToFile(fileName, symbol, symbol.packageName.asString(), file)
}
}
return symbols.filter { !it.validate() }.toList()
}
private fun isContainImplementClass(
symbols: Sequence,
className: String
): Boolean {
// logger.error("className = $className ,symbols= ${symbols}")
var isContainImplementClass = false
for (symbol in symbols) {
val annotationMap = getAnnotation(symbol)
val classMethodMap: MutableMap =
annotationMap["@ImplementClass"] ?: continue
val value: KSType? =
if (classMethodMap["value"] != null) classMethodMap["value"] as KSType else null
val targetClassName: String? =
(if (value != null) value.declaration.packageName.asString() + "." + value.toString() else null)
if (targetClassName == className){
if (symbol is KSClassDeclaration) {
val typeList = symbol.superTypes.toList()
for (ksTypeReference in typeList) {
val superClassName =
ksTypeReference.resolve().declaration.packageName.asString() + "." + ksTypeReference
if (superClassName == className) {
isContainImplementClass = true
break
}
}
if (!isContainImplementClass){
val thisName = symbol.packageName.asString() + "." + symbol
throw IllegalArgumentException("注意:实现类 $thisName,没有继承 $className")
}
}
}
}
return isContainImplementClass
}
private fun isImplementClass(
symbol: KSAnnotated,
className: String
): Boolean {
var isImplementClass = false
if (symbol is KSClassDeclaration) {
val typeList = symbol.superTypes.toList()
for (ksTypeReference in typeList) {
val superClassName =
ksTypeReference.resolve().declaration.packageName.asString() + "." + ksTypeReference
if (superClassName == className) {
isImplementClass = true
break
}
}
if (!isImplementClass){
val thisName = symbol.packageName.asString() + "." + symbol
throw IllegalArgumentException("注意:实现类 $thisName 的 @ImplementClass 注解设置的类为$className,但却没有没有继承 $className")
}
}
return isImplementClass
}
private fun getAnnotation(symbol: KSAnnotated): MutableMap?> {
val map = mutableMapOf?>()
for (annotation in symbol.annotations) {
val annotationName = annotation.toString()
var innerMap = map[annotationName]
if (innerMap == null) {
innerMap = mutableMapOf()
map[annotationName] = innerMap
}
for (argument in annotation.arguments) {
innerMap[argument.name?.getShortName().toString()] = argument.value as Any
}
}
return map
}
private fun whatsMyName(name: String): FunSpec.Builder {
return FunSpec.builder(name).addModifiers(KModifier.FINAL)
}
private fun writeToFile(
fileName: String, symbol: KSAnnotated, packageName: String, file: File
) {
FileInputStream(file).use { inputs ->
val bytes = inputs.readAllBytes()
codeGenerator
.createNewFile(
Dependencies(false, symbol.containingFile!!),
packageName,
fileName,
"api"
).write(bytes)
}
}
private fun writeToFile(
typeBuilder: TypeSpec.Builder,
packageName: String,
fileName: String,
symbol: KSAnnotated,
writeApi:Boolean = false,
writeKt:Boolean = true
) {
val typeSpec = typeBuilder.build()
val kotlinFile = FileSpec.builder(packageName, fileName).addType(typeSpec)
.build()
if (writeKt){
codeGenerator
.createNewFile(
Dependencies(false, symbol.containingFile!!),
packageName,
fileName
)
.writer()
.use {
kotlinFile.writeTo(it)
}
}
if (writeApi){
codeGenerator
.createNewFile(
Dependencies(false, symbol.containingFile!!),
packageName,
"$fileName.kt",
"api"
)
.writer()
.use {
kotlinFile.writeTo(it)
}
}
}
private fun writeToFile(
typeBuilder: TypeSpec.Builder,
packageName: String,
fileName: String,
writeApi:Boolean = false,
ksFiles : Array
) {
val typeSpec = typeBuilder.build()
val kotlinFile = FileSpec.builder(packageName, fileName).addType(typeSpec)
.build()
codeGenerator
.createNewFile(
Dependencies(false, *ksFiles),
packageName,
fileName
)
.writer()
.use {
kotlinFile.writeTo(it)
}
if (writeApi){
codeGenerator
.createNewFile(
Dependencies(false, *ksFiles),
packageName,
"$fileName.kt",
"api"
)
.writer()
.use {
kotlinFile.writeTo(it)
}
}
}
}