Don Phillips
12/23/2024, 7:44 PMKotlinUMethod
to find the return type of the method, resolve it into a UClass
(I think, but unimportant for this question) and then from there I can see if the class of the type is annotated with Moshi annotations properly.
Now that we wrote a custom Retrofit CallAdapter to allow us to adapt a Retrofit Call<T>
to a kotlin.Result<T>
, my lint check is returning a false negative. If you look at the attached screenshot of the debugger, you'll see that for the highlighted method definition in the left pane, node.returnType.canonicalText
is returning java.lang.Object
rather than kotlin.Result
,and the false negative happens b/c Retrofit technically can handle Object natively, but I don't want that here. however, if I use the node
to grab the sourcePsi
prop and drill down to the source definition of the return value, I can see the Result
bit there in the source that was parsed.
From what I've found so far, I'm basically stuck b/c kotlin.Result
is an inline value class, soo the true type is lost when UAST/PSI resolves the source code. And I can't think of a good way to drop into the sourcePsi
and look at the source directly because it's all strings, seems brittle AF
So I ask, is there any way for me to know that KotlinUMethod.returnType
is a kotlin.Result<T>
?gmz
12/23/2024, 8:47 PMresultTest()
isn't a suspend fun
? I'm asking because a) I'd expect a suspend fun for something like this, b) that changes everything: https://cs.android.com/android-studio/platform/tools/base/+/mirror-goog-studio-main:lint/libs/lint-checks/src/main/java/com/android/tools/lint/checks/CheckResultDetector.kt;l=135-141;drc=c670bb919484bbf611c085c04c36287276c018d4gmz
12/23/2024, 8:51 PMgmz
12/23/2024, 8:53 PMgetSuspendFunctionReturnTypeAsPsiClassReference()
gmz
12/23/2024, 9:30 PMResult<String>
, but Object
(disclaimer: I did not try it), so would this even work as expected without the suspend modifier?Don Phillips
12/23/2024, 10:08 PMfun UMethod.getSuspendFunctionReturnTypeAsPsiClassReference(): PsiClassReferenceType? =
// Suspending function signatures are rewritten by the compiler to a regular function that returns
// Unit, and adds a parameter named $continuation to the function's parameter list. This continuation
// parameter has a type of kotlin.coroutines.Continuation<? super T>, where T is the return type of
// the original suspend function definition.
uastParameters.find { uParameter ->
// Type of $completion parameter is kotlin.coroutines.Continuation
val uParameterType = uParameter.type
if (!uParameterType.canonicalText.startsWith("kotlin.coroutines.Continuation")) return@find false
// Continuation parameter's type parameter must be "? super (return type of suspend fun)"
val continuationTypeParameter = uParameterType as? PsiClassReferenceType ?: return@find false
val continuationWildcardTypeParameter =
continuationTypeParameter.parameters[0] as? PsiWildcardType
?: return@find false
// I don't think this is possible, but just in case, make sure the super bound != null
continuationWildcardTypeParameter.superBound != PsiTypes.nullType()
}?.let { continuationParameter ->
// Extract the super bound type from the Continuation parameter
val continuationParameterWildcardTypeParameter =
// No null checking b/c we know it's there from the previous find step
(continuationParameter.type as PsiClassReferenceType).parameters[0] as PsiWildcardType
continuationParameterWildcardTypeParameter.superBound as PsiClassReferenceType
}
And from what I've found, this somehow finds the type just fine. I didn't show that case in the screenshot, but debugging that problem showed that the type of the continuation
parameter was Result? super X. I haven't dug deeply into that part yet.
You may also be right about these test cases not being valid. TBH I speedrun'd learning linting writing this check and it was a lot to grok and I probably have some invalid test cases here.Don Phillips
12/23/2024, 10:10 PMgmz
12/23/2024, 11:15 PMBut I am not sure it needs to be a suspend fun at all times, because I am 99% sure this call adapter we wrote for Result works in concert w/ the call adapter for suspending functionsRetrofit does more or less what your lint check is doing, it gets the return type of each method using reflection. It then uses that information to find a call adapters that can handle it. This is why I think this won't work if you use a regular function, retrofit should only see Object, given that
Result
does not exist at runtime.
It works with suspend functions because the type gets encoded in the Continuation
and it can get it from there, just like your lint check does.
That said, if you don't make that a suspend fun, then the only way to get a Result
from it is by making that a blocking call, which is unexpected.gmz
12/23/2024, 11:15 PMDon Phillips
12/23/2024, 11:20 PMfun getFoo(): Call<Result<Foo>>
.Don Phillips
12/23/2024, 11:21 PMRetrofit
instance when building it w/ validateEagerly(true)
and then when you call retrofit.create(FooService::class.java)
it'll make sure everything is good, including making sure it can get a json converter.
I just tested this, and it seems to work. Create a unit test, with a retrofit instance with a Moshi converter factory with only generated adapters, Try to create a service where Foo
isn't annotated w/ @JsonClass(generateAdapter = true)
in the method sig, test fails with an exception from Moshi saying it can't find the adaptergmz
12/23/2024, 11:25 PMgmz
12/23/2024, 11:26 PMgmz
12/23/2024, 11:26 PMDon Phillips
12/23/2024, 11:30 PMDon Phillips
12/23/2024, 11:52 PMDavid Rawson
12/25/2024, 7:54 AMKtTypeElement
with:
((node.sourcePsi.lastChild as KtTypeReference).lastChild as KtTypeElement)
From there, you can get the type arguments (String
from Result<String>
) and so on.gmz
12/26/2024, 6:49 PMKtTypeReference
I can retrieve the original class, for example: (method.sourcePsi?.lastChild as? KtTypeReference)?.getType()?.getKotlinTypeFqName(false)
(but I've just noticed that getType
is internal)