
moxy.compiler.viewstate.ViewInterfaceProcessor.kt Maven / Gradle / Ivy
package moxy.compiler.viewstate
import com.squareup.javapoet.JavaFile
import moxy.compiler.ElementProcessor
import moxy.compiler.MvpCompiler
import moxy.compiler.MvpCompiler.Companion.elementUtils
import moxy.compiler.MvpCompiler.Companion.messager
import moxy.compiler.asDeclaredType
import moxy.compiler.asTypeElement
import moxy.compiler.getAnnotationMirror
import moxy.compiler.getValueAsString
import moxy.compiler.getValueAsTypeMirror
import moxy.compiler.viewstate.entity.MigrationMethod
import moxy.compiler.viewstate.entity.StrategyWithTag
import moxy.compiler.viewstate.entity.ViewInterfaceInfo
import moxy.compiler.viewstate.entity.ViewInterfaceMethod
import moxy.compiler.viewstate.entity.ViewStateMethod
import moxy.viewstate.strategy.AddToEndSingleStrategy
import moxy.viewstate.strategy.StateStrategyType
import javax.lang.model.element.AnnotationMirror
import javax.lang.model.element.Element
import javax.lang.model.element.ElementKind
import javax.lang.model.element.ExecutableElement
import javax.lang.model.element.Modifier
import javax.lang.model.element.TypeElement
import javax.lang.model.type.TypeKind
import javax.lang.model.type.TypeMirror
import javax.tools.Diagnostic.Kind
class ViewInterfaceProcessor(
private val disableEmptyStrategyCheck: Boolean,
private val enableEmptyStrategyHelper: Boolean,
defaultStrategy: String?
) : ElementProcessor {
private val frameworkDefaultStrategy: TypeElement
private lateinit var viewInterfaceElement: TypeElement
private val migrationMethods = mutableListOf()
init {
if (defaultStrategy != null) {
var localDefaultStrategy: TypeElement? = elementUtils.getTypeElement(defaultStrategy)
if (localDefaultStrategy == null) {
messager.printMessage(
Kind.ERROR,
"Unable to parse option '$OPTION_DEFAULT_STRATEGY'. Check $defaultStrategy exists"
)
localDefaultStrategy = DEFAULT_STATE_STRATEGY
}
frameworkDefaultStrategy = localDefaultStrategy
} else {
frameworkDefaultStrategy = DEFAULT_STATE_STRATEGY
}
}
fun makeMigrationHelper(): JavaFile? {
return if (enableEmptyStrategyHelper && migrationMethods.isNotEmpty()) {
EmptyStrategyHelperGenerator.generate(migrationMethods)
} else {
null
}
}
override fun process(element: TypeElement): ViewInterfaceInfo {
this.viewInterfaceElement = element
// Get methods from input interface
val viewInterfaceMethods = getMethods(element)
validateSuperInterfaceMethodClashes(viewInterfaceMethods)
val methods = validateForEmptyStrategies(viewInterfaceMethods)
addUniqueSuffixToMethodsWithTheSameName(methods)
return ViewInterfaceInfo(element, methods.toList())
}
/**
* Returns ViewMethod for each suitable method from this interface and its superinterfaces
*/
private fun getMethods(element: TypeElement): Set {
// Get methods from input class
val enclosedMethods = getEnclosedMethods(element)
// Get methods from super interfaces
val methodsFromSuperinterfaces = iterateInterfaces(element)
// Combine and exclude overridden methods
return combineMethods(enclosedMethods, methodsFromSuperinterfaces)
}
/**
* Returns ViewMethod for each suitable enclosed method into this interface (but not superinterface)
*/
private fun getEnclosedMethods(viewInterface: TypeElement): Set {
val viewInterfaceStrategy = getInterfaceStateStrategyType(viewInterface)
return viewInterface.enclosedElements
.filter {
// ignore all but non-static methods
it.kind == ElementKind.METHOD && !it.isStatic()
}
.map { getViewMethod(it as ExecutableElement, viewInterface, viewInterfaceStrategy) }
.toSet()
}
private fun getViewMethod(
methodElement: ExecutableElement,
viewInterface: TypeElement,
viewInterfaceStrategyType: TypeElement?
): ViewInterfaceMethod {
if (methodElement.returnType.kind != TypeKind.VOID) {
val message = "You are trying to generate ViewState for ${viewInterface.simpleName}. " +
"But ${viewInterface.simpleName} contains non-void method \"${methodElement.simpleName}\" " +
"with the return type of ${methodElement.returnType}."
messager.printMessage(Kind.ERROR, message, methodElement)
}
val strategy: StrategyWithTag? = getStateStrategy(methodElement)
?: viewInterfaceStrategyType?.let { type -> StrategyWithTag(type, methodElement.defaultTag()) }
return ViewInterfaceMethod(
viewInterfaceElement.asDeclaredType(),
viewInterface.asDeclaredType(),
methodElement,
strategy
)
}
private fun getStateStrategy(methodElement: ExecutableElement): StrategyWithTag? {
val annotation = getStateStrategyAnnotation(methodElement) ?: return null
val strategyClassFromAnnotation = annotation.getValueAsTypeMirror(StateStrategyType::value) ?: return null
val strategyType = strategyClassFromAnnotation.asTypeElement()
val tag = annotation.getValueAsString(StateStrategyType::tag) ?: methodElement.defaultTag()
return StrategyWithTag(strategyType, tag)
}
/**
* Returns ViewMethods for all superinterfaces, or empty set if element does not have superinterfaces
*/
private fun iterateInterfaces(
viewInterface: TypeElement
): List> {
return viewInterface.interfaces
.map {
getTypeAndValidateGenerics(it)
}
.map {
// implicit recursion
getMethods(it)
}
}
private fun getTypeAndValidateGenerics(interfaceMirror: TypeMirror): TypeElement {
val superinterface = interfaceMirror.asTypeElement()
val typeArguments = interfaceMirror.typeArguments
val typeParameters = superinterface.typeParameters
require(typeArguments.size <= typeParameters.size) {
"Code generation for the interface ${superinterface.simpleName} failed. Simplify your generics."
}
return superinterface
}
/**
* Combines methods, discarding duplicates from superinterface
*/
private fun combineMethods(
methods: Set,
superInterfaces: List>
): Set {
val superInterfaceMethods = combineMethodsFromSuperinterfaces(superInterfaces)
// order is very important. Refer to Set.add() and Set.addAll() for more info
return methods + superInterfaceMethods
}
private fun combineMethodsFromSuperinterfaces(
superInterfaceMethods: List>
): Set {
val resultSet = mutableSetOf()
for (superInterface in superInterfaceMethods) {
for (method in superInterface) {
val isAdded = resultSet.add(method)
if (!isAdded) {
val contained = resultSet.first { it == method }
contained.superInterfaceClash = method
}
}
}
return resultSet
}
private fun reportSuperinterfaceMethodsClash(methodA: ViewInterfaceMethod, methodB: ViewInterfaceMethod) {
if (methodA.strategy != methodB.strategy
&& methodA.strategy != null
&& methodB.strategy != null
) {
messager.printMessage(
Kind.ERROR,
"Strategy clash in superinterfaces of $viewInterfaceElement. " +
"Interface ${methodB.enclosingInterfaceElement.toString()} defines ${methodB.signature} " +
"with strategy ${methodB.strategy.strategyClass.simpleName}, " +
"but ${methodA.enclosingInterfaceElement.toString()} defines this method " +
"with strategy ${methodA.strategy.strategyClass.simpleName}. " +
"Override this method in $viewInterfaceElement to choose appropriate strategy",
viewInterfaceElement
)
}
}
private fun addUniqueSuffixToMethodsWithTheSameName(methods: List) {
// Allow methods to have equal names
val methodsCounter = mutableMapOf()
for (method in methods) {
val counter = methodsCounter[method.name] ?: 0
if (counter > 0) {
method.uniqueSuffix = counter.toString()
}
methodsCounter[method.name] = counter + 1
}
}
/**
* Returns default StateStrategyType for this [viewInterface], if specified
*/
private fun getInterfaceStateStrategyType(viewInterface: TypeElement): TypeElement? {
val annotation = getStateStrategyAnnotation(viewInterface)
val value = annotation?.getValueAsTypeMirror(StateStrategyType::value)
return if (value != null && value.kind == TypeKind.DECLARED) {
value.asTypeElement()
} else {
null
}
}
private fun validateSuperInterfaceMethodClashes(viewInterfaceMethods: Set) {
for (method in viewInterfaceMethods) {
method.superInterfaceClash?.let { clashed ->
reportSuperinterfaceMethodsClash(clashed, method)
}
}
}
private fun validateForEmptyStrategies(
methods: Set
): List {
return methods.map { method ->
if (method.strategy == null) {
reportEmptyStrategy(method.methodElement)
method.toViewMethod(StrategyWithTag(frameworkDefaultStrategy, method.methodElement.defaultTag()))
} else {
method.toViewMethod()
}
}
}
private fun reportEmptyStrategy(methodElement: ExecutableElement) {
if (!disableEmptyStrategyCheck) {
if (enableEmptyStrategyHelper) {
migrationMethods.add(MigrationMethod(viewInterfaceElement, methodElement))
} else {
val message = ("A View method has no strategy! " +
"Add @StateStrategyType annotation to this method, or to the View interface. " +
"You can also specify default strategy via compiler option.")
messager.printMessage(Kind.ERROR, message, methodElement)
}
}
}
private fun getStateStrategyAnnotation(element: Element): AnnotationMirror? {
val enclosed = listOfNotNull(element.getAnnotationMirror(StateStrategyType::class))
val aliased = element.annotationMirrors
.mapNotNull { it.annotationType.asTypeElement().getAnnotationMirror(StateStrategyType::class) }
val strategies = enclosed + aliased
if (strategies.size > 1) {
if (element is ExecutableElement) {
messager.printMessage(
Kind.ERROR, "There's more than one state strategy type defined for method " +
"'${element.simpleName}(${element.parameters.joinToString {
it.asType().toString()
}})'" +
" in interface '${element.enclosingElement.asType()}'", element
)
} else if (element is TypeElement) {
messager.printMessage(
Kind.ERROR, "There's more than one state strategy type defined for " +
"'${element.simpleName}'", element
)
}
}
return strategies.firstOrNull()
}
private fun ExecutableElement.defaultTag(): String = simpleName.toString()
private fun Element.isStatic(): Boolean {
return modifiers.contains(Modifier.STATIC)
}
companion object {
private const val OPTION_DEFAULT_STRATEGY = MvpCompiler.DEFAULT_MOXY_STRATEGY
private val DEFAULT_STATE_STRATEGY: TypeElement =
elementUtils.getTypeElement(AddToEndSingleStrategy::class.java.canonicalName)
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy