kotlinpoet: TypeMirror.asTypeName() returns java.lang.String when receiver type is kotlin.String

Overview

Thx for amazing library!

I encountered a problem that TypeMirror.asTypeName() returns java.lang.String when receiver type is kotlin.String. It seems this is a bug so currently we inserted a workaround below to avoid that.

fun TypeName.correctStringType() =
    if (this.toString() == "java.lang.String") ClassName("kotlin", "String") else this

About this issue

  • Original URL
  • State: closed
  • Created 7 years ago
  • Reactions: 13
  • Comments: 48 (4 by maintainers)

Commits related to this issue

Most upvoted comments

@richardwrq @tschuchortdev This code is ok also for generic classes like List:

fun Element.javaToKotlinType(): TypeName =
        asType().asTypeName().javaToKotlinType()

 fun TypeName.javaToKotlinType(): TypeName {
        return if (this is ParameterizedTypeName) {
            ParameterizedTypeName.get(
                rawType.javaToKotlinType() as ClassName,
                *typeArguments.map { it.javaToKotlinType() }.toTypedArray()
            )
        } else {
            val className =
                JavaToKotlinClassMap.INSTANCE.mapJavaToKotlin(FqName(toString()))
                    ?.asSingleFqName()?.asString()

            return if (className == null) {
                this
            } else {
                ClassName.bestGuess(className)
            }
        }
    }
``

Please ask usage questions on stackoverflow. Given the general availability of metadata support, I’m going to close this issue out.

@JakeWharton Any news on this one?

Thx!

I understood the rationale and let me elaborate the situation I encountered. Think about generating some pieces of code with annotation processor(kapt) from below.

@MarkerAnnotation
fun hoge(arg: String) {
  // do something
}

Then we expect such code is generated with KotlinPoet:

fun hogeWithKapt(arg: String) {
  // arg type is expected to be kotlin.String
}

But now we’ve got the code like:

fun hogeWithKapt(arg: java.lang.String) {
  // IDE flags warning about argument to use kotlin.String
}

Sorry I forgot to mention but our workaround is just for our library, so we don’t have to consider any side effect about it(I didn’t intend to add it to KotlinPoet for sure).

Anyway I realized it’s kind of counter-intuitive because it means KotlinPoet user always take care about replacing java String to kotlin String. I don’t come up with good way to deal with but I just reported as feedback 🙇

Please close this issue if it’s not fit for your plan.

@StefMa @zigzago List<List<String>> is converted to kotlin.collections.List<out java.util.List<java.lang.String>> at my side (kotlin version: 1.3.50&1.3.60). And I find the reason: the first List is ParameterizedTypeName so that it can be handled properly by javaToKotlinType(). However, the second List is actually WildcardTypeName (note the out) so that its converted type is just guessed by ClassName.

Interestingly, if we have a Map<List<Int>, List<Int>>, it will be converted to kotlin.collections.Map<kotlin.collections.List<kotlin.Int>, out java.util.List<java.lang.Integer>>>, which means the first List is regarded as ParameterizedTypeName correctly, but at the same time the second is WildcardTypeName.

As a result, we can improve javaToKotlinType() for WildcardTypeName case like this:

  fun TypeName.javaToKotlinType(): TypeName {
    return when (this) {
      is ParameterizedTypeName -> {
        (rawType.javaToKotlinType() as ClassName).parameterizedBy(*(typeArguments.map { it.javaToKotlinType() }.toTypedArray()))
      }
      is WildcardTypeName -> {
        outTypes[0].javaToKotlinType()
      }
      else -> {
        val className = JavaToKotlinClassMap.INSTANCE.mapJavaToKotlin(FqName(toString()))?.asSingleFqName()?.asString()
        return if (className == null) {
          this
        } else {
          ClassName.bestGuess(className)
        }
      }
    }
  }

And now it should work while I didn’t test it thoroughly.

The metadata artifacts are separate and were released in 1.4.0. There are full READMEs in their artifacts on the repo.

It’s important to understand that metadata annotations are only present on classes. Not individual functions, types, properties, parameters, or anywhere else.

In short: cases like TypeMirror.asTypeName() or Type.asTypeName() will never work as intended for Kotlin compiler intrinsic types. APIs like this simply cannot look at individual types in isolation and have enough context to understand them. You must have access to the appropriate metadata that describes them in context. There are no silver bullets here, this is how Kotlin works.

You can, however, derive the metadata context from navigable elements like methods, fields, etc. You simply traverse up its hierarchy to the first enclosing class, which will have the needed metadata annotation on it. Then you can connect that parsed information back to your source element and connect the dots to deduce the correct type. This is exactly how the snippet I provided above for overriding works, and it works with the 1.4.0 metadata release.

There is possibly an argument to be made that either kotlinpoet-metadata artifacts should live directly in the main KotlinPoet artifact, or the existing problematic APIs should be deprecated and moved into the kotlinpoet-metadata artifact where they can ask for the needed classpath information. At this point I think that’s a separate issue.

@CasualGitEnjoyer the artifacts have been renamed, which invalidated the old URLs. Here’s an up-to-date link for interop-kotlinx-metadata docs: https://square.github.io/kotlinpoet/interop-kotlinx-metadata/.

Are there any documentation for kotlinpet metadata anywhere?

Did you look at their docs on the project site?

https://square.github.io/kotlinpoet/kotlinpoet_metadata/

https://square.github.io/kotlinpoet/kotlinpoet_metadata_specs/

For any future reader here is a very interesting talk about metadata @JohnOberhauser https://www.youtube.com/watch?v=uHPti6Z02tI

@ZacSweers are there any examples on how to use that?

Is there a “correct” way to handle this yet?

A map is not the correct nor accurate solution to this problem since it’s not a 1:1 mapping. Multiple distinct Kotlin types are represented as the same JVM type and it’s only exacerbated by inline classes. Parsing the metadata is the only way forward.

Api has changed a bit (use parameterizedBy instead of ParameterizedTypeName.get):

private fun TypeName.javaToKotlinType(): TypeName = if (this is ParameterizedTypeName) {
    (rawType.javaToKotlinType() as ClassName).parameterizedBy(
        *typeArguments.map { it.javaToKotlinType() }.toTypedArray()
    )
} else {
    val className = JavaToKotlinClassMap.INSTANCE
        .mapJavaToKotlin(FqName(toString()))?.asSingleFqName()?.asString()
    if (className == null) this
    else ClassName.bestGuess(className)
}

after 1.0.0-RC1 the above answer will need some changes in order to work, these lines to be specific.

ParameterizedTypeName.get(rawType.javaToKotlinType() as ClassName, *typeArguments.map { it.javaToKotlinType() }.toTypedArray())

====>

import com.squareup.kotlinpoet.ParameterizedTypeName.Companion.parameterizedBy
...
(rawType.javaToKotlinType() as ClassName).parameterizedBy(*typeArguments.map { it.javaToKotlinType() }.toTypedArray())

this was suggested on #424 .