JavaPoetExt.kt

/*
 * Copyright (C) 2020 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package androidx.room.compiler.processing

import java.lang.Character.isISOControl
import com.squareup.javapoet.AnnotationSpec
import com.squareup.javapoet.ClassName
import com.squareup.javapoet.MethodSpec
import com.squareup.javapoet.ParameterSpec
import com.squareup.javapoet.ParameterizedTypeName
import com.squareup.javapoet.TypeName
import com.squareup.javapoet.TypeSpec
import javax.lang.model.SourceVersion
import javax.lang.model.element.Modifier
import javax.lang.model.type.TypeKind
import javax.lang.model.type.TypeMirror

/**
 * Javapoet does not model NonType, unlike javac, which makes it hard to rely on TypeName for
 * common functionality (e.g. ability to implement XType.isLong as typename() == TypeName.LONG
 * instead of in the base class)
 *
 * For those cases, we have this hacky type so that we can always query TypeName on an XType.
 *
 * We should still strive to avoid these cases, maybe turn it to an error in tests.
 */
private val NONE_TYPE_NAME = ClassName.get("androidx.room.compiler.processing.error", "NotAType")

fun XAnnotation.toAnnotationSpec(): AnnotationSpec {
  val builder = AnnotationSpec.builder(className)
  annotationValues.forEach { builder.addAnnotationValue(it) }
  return builder.build()
}

private fun AnnotationSpec.Builder.addAnnotationValue(annotationValue: XAnnotationValue) {
  annotationValue.apply {
    requireNotNull(value) { "value == null, constant non-null value expected for $name" }
    require(SourceVersion.isName(name)) { "not a valid name: $name" }
    when {
      hasListValue() -> asAnnotationValueList().forEach { addAnnotationValue(it) }
      hasAnnotationValue() -> addMember(name, "\$L", asAnnotation().toAnnotationSpec())
      hasEnumValue() -> addMember(
        name, "\$T.\$L", asEnum().enclosingElement.className, asEnum().name
      )
      hasTypeValue() -> addMember(name, "\$T.class", asType().typeName)
      hasStringValue() -> addMember(name, "\$S", asString())
      hasFloatValue() -> addMember(name, "\$Lf", asFloat())
      hasCharValue() -> addMember(
        name, "'\$L'", characterLiteralWithoutSingleQuotes(asChar())
      )
      else -> addMember(name, "\$L", value)
    }
  }
}

private fun characterLiteralWithoutSingleQuotes(c: Char): String? {
    // see https://docs.oracle.com/javase/specs/jls/se7/html/jls-3.html#jls-3.10.6
    return when (c) {
        '\b' -> "\b" /* \u0008: backspace (BS) */
        '\t' -> "\t" /* \u0009: horizontal tab (HT) */
        '\n' -> "\n" /* \u000a: linefeed (LF) */
        '\u000c' -> "\u000c" /* \u000c: form feed (FF) */
        '\r' -> "\r" /* \u000d: carriage return (CR) */
        '\"' -> "\"" /* \u0022: double quote (") */
        '


' -> "\'" /* \u0027: single quote (') */ '\' -> "\\" /* \u005c: backslash (\) */ else -> if (isISOControl(c)) String.format("\u%04x", c.code) else Character.toString(c) } } internal fun TypeMirror.safeTypeName(): TypeName = if (kind == TypeKind.NONE) { NONE_TYPE_NAME } else { TypeName.get(this) } /** * Adds the given element as the originating element for compilation. * see [TypeSpec.Builder.addOriginatingElement]. */ fun TypeSpec.Builder.addOriginatingElement(element: XElement): TypeSpec.Builder { element.originatingElementForPoet()?.let(this::addOriginatingElement) return this } internal fun TypeName.rawTypeName(): TypeName { return if (this is ParameterizedTypeName) { this.rawType } else { this } } /** * Returns the unboxed TypeName for this if it can be unboxed, otherwise, returns this. */ internal fun TypeName.tryUnbox(): TypeName { return if (isBoxedPrimitive) { unbox() } else { this } } /** * Returns the boxed TypeName for this if it can be unboxed, otherwise, returns this. */ internal fun TypeName.tryBox(): TypeName { return try { box() } catch (err: AssertionError) { this } } /** * Helper class to create overrides for XExecutableElements with final parameters and correct * parameter names read from Kotlin Metadata. */ object MethodSpecHelper { /** * Creates an overriding [MethodSpec] for the given [XMethodElement] that * does everything in [overriding] and also mark all parameters as final */ @JvmStatic fun overridingWithFinalParams( elm: XMethodElement, owner: XType ): MethodSpec.Builder { val asMember = elm.asMemberOf(owner) return overriding( executableElement = elm, resolvedType = asMember, Modifier.FINAL ) } /** * Creates an overriding [MethodSpec] for the given [XMethodElement] where: * * parameter names are copied from KotlinMetadata when available * * [Override] annotation is added and other annotations are dropped * * thrown types are copied if the backing element is from java */ @JvmStatic fun overriding( elm: XMethodElement, owner: XType ): MethodSpec.Builder { val asMember = elm.asMemberOf(owner) return overriding( executableElement = elm, resolvedType = asMember ) } private fun overriding( executableElement: XMethodElement, resolvedType: XMethodType = executableElement.executableType, vararg paramModifiers: Modifier ): MethodSpec.Builder { return MethodSpec.methodBuilder(executableElement.jvmName).apply { addTypeVariables( resolvedType.typeVariableNames ) resolvedType.parameterTypes.forEachIndexed { index, paramType -> addParameter( ParameterSpec.builder( paramType.typeName, executableElement.parameters[index].name, *paramModifiers ).build() ) } if (executableElement.isPublic()) { addModifiers(Modifier.PUBLIC) } else if (executableElement.isProtected()) { addModifiers(Modifier.PROTECTED) } addAnnotation(Override::class.java) varargs(executableElement.isVarArgs()) executableElement.thrownTypes.forEach { addException(it.typeName) } returns(resolvedType.returnType.typeName) } } }