Is there a plan to improve support for <delegation...
# ksp
u
Is there a plan to improve support for delegations? Processing a class that has a delegation with KSP doesn’t provide all the expected information. For example:
Copy code
@KspTestingAnnotation
class KspTesterDelegation : MainInterface by MainImpl() {
  fun method2(): Int {
    return 10
  }
}

interface MainInterface {
  fun method1(): Int
}

class MainImpl : MainInterface {
  override fun method1(): Int {
    return 4
  }
}
Getting all the declarations of
KspTesterDelegation
will only provide its constructor and
method2()
, while in KAPT we also get
method1()
. I know the reason KAPT can do that is because the generated stub has the delegated methods inside the main class. Is there a way I can still get the methods of a delegated class when using KSP?
y
well technically it is not declared in that file 😕 . Is there a case where this matters? (vs getting the properties / methods in super interface as member of your class?)
u
I’m working on a KSP adapter that creates Java Elements objects after processing classes using KSP, in order to use the same Java annotation processors with KSP and with minimal changes. For that to work, I need to get the results as close as possible to the ones generated by KAPT. If the TypeElement object I’m generating doesn’t have the same methods as it does when using KAPT - it’ll break the existing annotation processor.
I was considering just adding the methods in the interface the class is implementing, but since I have no indication that there’s a delegation going on - I would need to use some heuristics to make sure that’s the case. I’m considering doing the following:
Copy code
if (!classDeclaration.modifiers.contains(Modifier.ABSTRACT)) {
  classDeclaration.superTypes.forEach {
    val methodsFromDelegation = mutableListOf<KSFunctionDeclaration>()
    val superType = it.resolve().declaration
    if (superType is KSClassDeclaration && superType.classKind == ClassKind.INTERFACE) {
      superType.getDeclaredFunctions().forEach { method -> methodsFromDelegation.add(method) }
    }
  }
}

// Add all methods in methodsFromDelegation to the enclosed elements of the main class, in case they're not there.
Which does: - Get all the super types of the class and iterate only the interfaces - Get all the methods of the interface - For each method, if the method doesn’t already exists in the class (i.e. it’s overridden and implemented) then add it to the list of enclosed elements. It’s similar to the way X-Processing is adding synthetic getter/setter methods. If the class is abstract then we can’t tell whether the interface methods will be added by a delegation or not, since KSP doesn’t provide any info that tells me there’s a delegation declared (i.e.
by MainImpl()
). I chose to skip abstract classes for now. Does this makes sense? Am I missing something? Is there a better way to do this?
y
possible. what you are trying to do is very similar to what we do in xprocessing, but we have our own abstraction vs java elements. initially that abstraction focused on making it look like kapt as much as possible. now that is complete, we are backtracking from it to make them look different when it makes sense. but i guess that is not an option for you when you want to use java elements API.
for this case, looks like we dont: https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:room/roo[…]piler/processing/ksp/KspTypeElement.kt;l=249?q=KspTypeElement (it doesn't matter for room whether something is declared or inherited)
u
Yeah exactly, I’m trying to keep using the original Java annotation processing code and get it to work with KSP and also with regular JavaAP. I looked at X-Processing as an option, but I want to avoid changing our code to fit a new abstraction. We have more than 10 annotation processors that I want to onboard with KSP and so far I got one of them to work using the KSP adapter that I’m developing. I did get a lot of ideas from X-Processing, so thanks for that 🙂 I hope I could also open-source the adapter I’m working on sometime soon.
Just to close the loop, here’s the implementation that’s working for me:
Copy code
// Inside KspTypeElement, which extends javax.lang.model.element.TypeElement

// Called by getEnclosedElements()
/**
* Gets methods from a delegated object
* 
* Example:
* // The methods in CoolInterface will be returned
* class ClassToProcessWithKsp : CoolInterface by CoolImpl() {...}
*
* Notes:
* - This will skip abstract classes since it can't be sure child classes aren't implementing
*    interface methods.
* - The methods will have an @Override annotation, as expected in a KAPT output.
*/
private fun getMethodsFromDelegation(): List<KspExecutableElement> {
val methodsFromDelegation = mutableListOf<KSFunctionDeclaration>()

// kspInternalObject is a KSClassDeclaration
if (!kspInternalObject.modifiers.contains(com.google.devtools.ksp.symbol.Modifier.ABSTRACT)) {
  kspInternalObject.superTypes.forEach {
    if (it.origin != Origin.JAVA) {
      val superType = it.resolve().declaration
      if (superType is KSClassDeclaration && superType.classKind == ClassKind.INTERFACE) {
        superType.getDeclaredFunctions().forEach { method -> methodsFromDelegation.add(method) }
      }
    }
  }
}
if (methodsFromDelegation.isNotEmpty()) {
  kspInternalObject.declarations.forEach {
    if (it is KSFunctionDeclaration) {
      // TODO expensive, find more ways to avoid calling if possible
      val overrideeFunc = it.findOverridee()
      if (methodsFromDelegation.contains(overrideeFunc)) {
        methodsFromDelegation.remove(overrideeFunc)
      }
    }
  }
}

return methodsFromDelegation
    .map {
      object : KspExecutableElement(it, this) {
        // Since these methods come from a super type, we need to add a (synthetic) @Override
        override fun getAnnotationMirrors(): List<AnnotationMirror> {
          return listOf(KspSyntheticOverrideAnnotationMirror())
        }
      }
    }
    .toList()
}