1 /* 2 * Copyright (C) 2015 Square, Inc. 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * https://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 package com.squareup.kotlinpoet 17 18 import com.google.common.truth.Truth.assertThat 19 import com.google.testing.compile.CompilationRule 20 import com.squareup.kotlinpoet.KModifier.OVERRIDE 21 import com.squareup.kotlinpoet.ParameterizedTypeName.Companion.parameterizedBy 22 import com.squareup.kotlinpoet.ParameterizedTypeName.Companion.plusParameter 23 import java.lang.annotation.Inherited 24 import kotlin.reflect.KClass 25 import kotlin.test.Test 26 import org.junit.Rule 27 28 class AnnotationSpecTest { 29 30 @Retention(AnnotationRetention.RUNTIME) 31 annotation class AnnotationA 32 33 @Inherited 34 @Retention(AnnotationRetention.RUNTIME) 35 annotation class AnnotationB 36 37 @Retention(AnnotationRetention.RUNTIME) 38 annotation class AnnotationC(val value: String) 39 40 enum class Breakfast { 41 WAFFLES, PANCAKES; 42 toStringnull43 override fun toString(): String { 44 return "$name with cherries!" 45 } 46 } 47 48 @Retention(AnnotationRetention.RUNTIME) 49 annotation class HasDefaultsAnnotation( 50 val a: Byte = 5, 51 val b: Short = 6, 52 val c: Int = 7, 53 val d: Long = 8, 54 val e: Float = 9.0f, 55 val f: Double = 10.0, 56 val g: CharArray = ['\u0000', '\uCAFE', 'z', '€', 'ℕ', '"', '\'', '\t', '\n'], 57 val h: Boolean = true, 58 val i: Breakfast = Breakfast.WAFFLES, 59 val j: AnnotationA = AnnotationA(), 60 val k: String = "maple", 61 val l: KClass<out Annotation> = AnnotationB::class, 62 val m: IntArray = [1, 2, 3], 63 val n: Array<Breakfast> = [Breakfast.WAFFLES, Breakfast.PANCAKES], 64 val o: Breakfast, 65 val p: Int, 66 val q: AnnotationC = AnnotationC("foo"), 67 val r: Array<KClass<out Number>> = [Byte::class, Short::class, Int::class, Long::class], 68 ) 69 70 @HasDefaultsAnnotation( 71 o = Breakfast.PANCAKES, 72 p = 1701, 73 f = 11.1, 74 m = [9, 8, 1], 75 l = Override::class, 76 j = AnnotationA(), 77 q = AnnotationC("bar"), 78 r = [Float::class, Double::class], 79 ) 80 inner class IsAnnotated 81 82 @Rule @JvmField 83 val compilation = CompilationRule() 84 equalsAndHashCodenull85 @Test fun equalsAndHashCode() { 86 var a = AnnotationSpec.builder(AnnotationC::class.java).build() 87 var b = AnnotationSpec.builder(AnnotationC::class.java).build() 88 assertThat(a == b).isTrue() 89 assertThat(a.hashCode()).isEqualTo(b.hashCode()) 90 a = AnnotationSpec.builder(AnnotationC::class.java).addMember("value", "%S", "123").build() 91 b = AnnotationSpec.builder(AnnotationC::class.java).addMember("value", "%S", "123").build() 92 assertThat(a == b).isTrue() 93 assertThat(a.hashCode()).isEqualTo(b.hashCode()) 94 } 95 defaultAnnotationnull96 @Test fun defaultAnnotation() { 97 val name = IsAnnotated::class.java.canonicalName 98 val element = compilation.elements.getTypeElement(name) 99 val annotation = AnnotationSpec.get(element.annotationMirrors[0]) 100 101 assertThat(toString(annotation)).isEqualTo( 102 """ 103 |package com.squareup.tacos 104 | 105 |import com.squareup.kotlinpoet.AnnotationSpecTest 106 |import java.lang.Override 107 |import kotlin.Double 108 |import kotlin.Float 109 | 110 |@AnnotationSpecTest.HasDefaultsAnnotation( 111 | f = 11.1, 112 | j = AnnotationSpecTest.AnnotationA(), 113 | l = Override::class, 114 | m = arrayOf(9, 8, 1), 115 | o = AnnotationSpecTest.Breakfast.PANCAKES, 116 | p = 1_701, 117 | q = AnnotationSpecTest.AnnotationC(value = "bar"), 118 | r = arrayOf(Float::class, Double::class), 119 |) 120 |public class Taco 121 | 122 """.trimMargin(), 123 ) 124 } 125 defaultAnnotationWithImportnull126 @Test fun defaultAnnotationWithImport() { 127 val name = IsAnnotated::class.java.canonicalName 128 val element = compilation.elements.getTypeElement(name) 129 val annotation = AnnotationSpec.get(element.annotationMirrors[0]) 130 val typeBuilder = TypeSpec.classBuilder(IsAnnotated::class.java.simpleName) 131 typeBuilder.addAnnotation(annotation) 132 val file = FileSpec.get("com.squareup.kotlinpoet", typeBuilder.build()) 133 assertThat(file.toString()).isEqualTo( 134 """ 135 |package com.squareup.kotlinpoet 136 | 137 |import java.lang.Override 138 |import kotlin.Double 139 |import kotlin.Float 140 | 141 |@AnnotationSpecTest.HasDefaultsAnnotation( 142 | f = 11.1, 143 | j = AnnotationSpecTest.AnnotationA(), 144 | l = Override::class, 145 | m = arrayOf(9, 8, 1), 146 | o = AnnotationSpecTest.Breakfast.PANCAKES, 147 | p = 1_701, 148 | q = AnnotationSpecTest.AnnotationC(value = "bar"), 149 | r = arrayOf(Float::class, Double::class), 150 |) 151 |public class IsAnnotated 152 | 153 """.trimMargin(), 154 ) 155 } 156 emptyArraynull157 @Test fun emptyArray() { 158 val builder = AnnotationSpec.builder(HasDefaultsAnnotation::class.java) 159 builder.addMember("%L = %L", "n", "[]") 160 assertThat(builder.build().toString()).isEqualTo( 161 "" + 162 "@com.squareup.kotlinpoet.AnnotationSpecTest.HasDefaultsAnnotation(" + 163 "n = []" + 164 ")", 165 ) 166 builder.addMember("%L = %L", "m", "[]") 167 assertThat(builder.build().toString()).isEqualTo( 168 "" + 169 "@com.squareup.kotlinpoet.AnnotationSpecTest.HasDefaultsAnnotation(" + 170 "n = [], " + 171 "m = []" + 172 ")", 173 ) 174 } 175 reflectAnnotationnull176 @Test fun reflectAnnotation() { 177 val annotation = IsAnnotated::class.java.getAnnotation(HasDefaultsAnnotation::class.java) 178 val spec = AnnotationSpec.get(annotation) 179 180 assertThat(toString(spec)).isEqualTo( 181 """ 182 |package com.squareup.tacos 183 | 184 |import com.squareup.kotlinpoet.AnnotationSpecTest 185 |import java.lang.Override 186 |import kotlin.Double 187 |import kotlin.Float 188 | 189 |@AnnotationSpecTest.HasDefaultsAnnotation( 190 | f = 11.1, 191 | l = Override::class, 192 | m = arrayOf(9, 8, 1), 193 | o = AnnotationSpecTest.Breakfast.PANCAKES, 194 | p = 1_701, 195 | q = AnnotationSpecTest.AnnotationC(value = "bar"), 196 | r = arrayOf(Float::class, Double::class), 197 |) 198 |public class Taco 199 | 200 """.trimMargin(), 201 ) 202 } 203 reflectAnnotationWithDefaultsnull204 @Test fun reflectAnnotationWithDefaults() { 205 val annotation = IsAnnotated::class.java.getAnnotation(HasDefaultsAnnotation::class.java) 206 val spec = AnnotationSpec.get(annotation, true) 207 208 assertThat(toString(spec)).isEqualTo( 209 """ 210 |package com.squareup.tacos 211 | 212 |import com.squareup.kotlinpoet.AnnotationSpecTest 213 |import java.lang.Override 214 |import kotlin.Double 215 |import kotlin.Float 216 | 217 |@AnnotationSpecTest.HasDefaultsAnnotation( 218 | a = 5, 219 | b = 6, 220 | c = 7, 221 | d = 8, 222 | e = 9.0f, 223 | f = 11.1, 224 | g = arrayOf('\u0000', '쫾', 'z', '€', 'ℕ', '"', '\'', '\t', '\n'), 225 | h = true, 226 | i = AnnotationSpecTest.Breakfast.WAFFLES, 227 | j = AnnotationSpecTest.AnnotationA(), 228 | k = "maple", 229 | l = Override::class, 230 | m = arrayOf(9, 8, 1), 231 | n = arrayOf(AnnotationSpecTest.Breakfast.WAFFLES, AnnotationSpecTest.Breakfast.PANCAKES), 232 | o = AnnotationSpecTest.Breakfast.PANCAKES, 233 | p = 1_701, 234 | q = AnnotationSpecTest.AnnotationC(value = "bar"), 235 | r = arrayOf(Float::class, Double::class), 236 |) 237 |public class Taco 238 | 239 """.trimMargin(), 240 ) 241 } 242 useSiteTargetnull243 @Test fun useSiteTarget() { 244 val builder = AnnotationSpec.builder(AnnotationA::class) 245 assertThat(builder.build().toString()).isEqualTo( 246 "" + 247 "@com.squareup.kotlinpoet.AnnotationSpecTest.AnnotationA", 248 ) 249 builder.useSiteTarget(AnnotationSpec.UseSiteTarget.FIELD) 250 assertThat(builder.build().toString()).isEqualTo( 251 "" + 252 "@field:com.squareup.kotlinpoet.AnnotationSpecTest.AnnotationA", 253 ) 254 builder.useSiteTarget(AnnotationSpec.UseSiteTarget.GET) 255 assertThat(builder.build().toString()).isEqualTo( 256 "" + 257 "@get:com.squareup.kotlinpoet.AnnotationSpecTest.AnnotationA", 258 ) 259 builder.useSiteTarget(null) 260 assertThat(builder.build().toString()).isEqualTo( 261 "" + 262 "@com.squareup.kotlinpoet.AnnotationSpecTest.AnnotationA", 263 ) 264 } 265 deprecatedTestnull266 @Test fun deprecatedTest() { 267 val annotation = AnnotationSpec.builder(Deprecated::class) 268 .addMember("%S", "Nope") 269 .addMember("%T(%S)", ReplaceWith::class, "Yep") 270 .build() 271 272 assertThat(annotation.toString()).isEqualTo( 273 "" + 274 "@kotlin.Deprecated(\"Nope\", kotlin.ReplaceWith(\"Yep\"))", 275 ) 276 } 277 modifyMembersnull278 @Test fun modifyMembers() { 279 val builder = AnnotationSpec.builder(Deprecated::class) 280 .addMember("%S", "Nope") 281 .addMember("%T(%S)", ReplaceWith::class, "Yep") 282 283 builder.members.removeAt(1) 284 builder.members.add(CodeBlock.of("%T(%S)", ReplaceWith::class, "Nope")) 285 286 assertThat(builder.build().toString()).isEqualTo( 287 "" + 288 "@kotlin.Deprecated(\"Nope\", kotlin.ReplaceWith(\"Nope\"))", 289 ) 290 } 291 annotationStringsAreConstantnull292 @Test fun annotationStringsAreConstant() { 293 val text = "This is a long string with a newline\nin the middle." 294 val builder = AnnotationSpec.builder(Deprecated::class) 295 .addMember("%S", text) 296 297 assertThat(builder.build().toString()).isEqualTo( 298 "" + 299 "@kotlin.Deprecated(\"This is a long string with a newline\\nin the middle.\")", 300 ) 301 } 302 literalAnnotationnull303 @Test fun literalAnnotation() { 304 val annotationSpec = AnnotationSpec.builder(Suppress::class) 305 .addMember("%S", "Things") 306 .build() 307 308 val file = FileSpec.builder("test", "Test") 309 .addFunction( 310 FunSpec.builder("test") 311 .addStatement("%L", annotationSpec) 312 .addStatement("val annotatedString = %S", "AnnotatedString") 313 .build(), 314 ) 315 .build() 316 assertThat(file.toString().trim()).isEqualTo( 317 """ 318 |package test 319 | 320 |import kotlin.Suppress 321 | 322 |public fun test() { 323 | @Suppress("Things") 324 | val annotatedString = "AnnotatedString" 325 |} 326 """.trimMargin(), 327 ) 328 } 329 functionOnlyLiteralAnnotationnull330 @Test fun functionOnlyLiteralAnnotation() { 331 val annotation = AnnotationSpec 332 .builder(ClassName.bestGuess("Suppress")) 333 .addMember("%S", "UNCHECKED_CAST") 334 .build() 335 val funSpec = FunSpec.builder("operation") 336 .addStatement("%L", annotation) 337 .build() 338 339 assertThat(funSpec.toString().trim()).isEqualTo( 340 """ 341 |public fun operation() { 342 | @Suppress("UNCHECKED_CAST") 343 |} 344 """.trimMargin(), 345 ) 346 } 347 getOnVarargMirrorShouldNameValueArgnull348 @Test fun getOnVarargMirrorShouldNameValueArg() { 349 val myClazz = compilation.elements 350 .getTypeElement(KotlinClassWithVarargAnnotation::class.java.canonicalName) 351 val classBuilder = TypeSpec.classBuilder("Result") 352 353 myClazz.annotationMirrors.map { AnnotationSpec.get(it) } 354 .filter { 355 val typeName = it.typeName 356 return@filter typeName is ClassName && typeName.simpleName == "AnnotationWithArrayValue" 357 } 358 .forEach { 359 classBuilder.addAnnotation(it) 360 } 361 362 assertThat(toString(classBuilder.build()).trim()).isEqualTo( 363 """ 364 |package com.squareup.tacos 365 | 366 |import com.squareup.kotlinpoet.AnnotationSpecTest 367 |import java.lang.Object 368 |import kotlin.Boolean 369 | 370 |@AnnotationSpecTest.AnnotationWithArrayValue(value = arrayOf(Object::class, Boolean::class)) 371 |public class Result 372 """.trimMargin(), 373 ) 374 } 375 getOnVarargAnnotationShouldNameValueArgnull376 @Test fun getOnVarargAnnotationShouldNameValueArg() { 377 val annotation = KotlinClassWithVarargAnnotation::class.java 378 .getAnnotation(AnnotationWithArrayValue::class.java) 379 val classBuilder = TypeSpec.classBuilder("Result") 380 .addAnnotation(AnnotationSpec.get(annotation)) 381 382 assertThat(toString(classBuilder.build()).trim()).isEqualTo( 383 """ 384 |package com.squareup.tacos 385 | 386 |import com.squareup.kotlinpoet.AnnotationSpecTest 387 |import java.lang.Object 388 |import kotlin.Boolean 389 | 390 |@AnnotationSpecTest.AnnotationWithArrayValue(value = arrayOf(Object::class, Boolean::class)) 391 |public class Result 392 """.trimMargin(), 393 ) 394 } 395 396 @AnnotationWithArrayValue(Any::class, Boolean::class) 397 class KotlinClassWithVarargAnnotation 398 399 @Retention(AnnotationRetention.RUNTIME) 400 internal annotation class AnnotationWithArrayValue(vararg val value: KClass<*>) 401 annotationsWithTypeParametersnull402 @Test fun annotationsWithTypeParameters() { 403 // Example from https://kotlinlang.org/docs/tutorials/android-plugin.html 404 val externalClass = ClassName("com.squareup.parceler", "ExternalClass") 405 val externalClassSpec = TypeSpec.classBuilder(externalClass) 406 .addProperty( 407 PropertySpec.builder("value", Int::class) 408 .initializer("value") 409 .build(), 410 ) 411 .primaryConstructor( 412 FunSpec.constructorBuilder() 413 .addParameter("value", Int::class) 414 .build(), 415 ) 416 .build() 417 val externalClassParceler = ClassName("com.squareup.parceler", "ExternalClassParceler") 418 val parcel = ClassName("com.squareup.parceler", "Parcel") 419 val externalClassParcelerSpec = TypeSpec.objectBuilder(externalClassParceler) 420 .addSuperinterface( 421 ClassName("com.squareup.parceler", "Parceler") 422 .parameterizedBy(externalClass), 423 ) 424 .addFunction( 425 FunSpec.builder("create") 426 .addModifiers(OVERRIDE) 427 .addParameter("parcel", parcel) 428 .returns(externalClass) 429 .addStatement("return %T(parcel.readInt())", externalClass) 430 .build(), 431 ) 432 .addFunction( 433 FunSpec.builder("write") 434 .addModifiers(OVERRIDE) 435 .receiver(externalClass) 436 .addParameter("parcel", parcel) 437 .addParameter("flags", Int::class) 438 .addStatement("parcel.writeInt(value)") 439 .build(), 440 ) 441 .build() 442 val parcelize = ClassName("com.squareup.parceler", "Parcelize") 443 val typeParceler = ClassName("com.squareup.parceler", "TypeParceler") 444 val typeParcelerAnnotation = AnnotationSpec.builder( 445 typeParceler 446 .plusParameter(externalClass) 447 .plusParameter(externalClassParceler), 448 ) 449 .build() 450 val classLocalParceler = TypeSpec.classBuilder("MyClass") 451 .addAnnotation(parcelize) 452 .addAnnotation(typeParcelerAnnotation) 453 .addProperty( 454 PropertySpec.builder("external", externalClass) 455 .initializer("external") 456 .build(), 457 ) 458 .primaryConstructor( 459 FunSpec.constructorBuilder() 460 .addParameter("external", externalClass) 461 .build(), 462 ) 463 .build() 464 val propertyLocalParceler = TypeSpec.classBuilder("MyClass") 465 .addAnnotation(parcelize) 466 .addProperty( 467 PropertySpec.builder("external", externalClass) 468 .addAnnotation(typeParcelerAnnotation) 469 .initializer("external") 470 .build(), 471 ) 472 .primaryConstructor( 473 FunSpec.constructorBuilder() 474 .addParameter("external", externalClass) 475 .build(), 476 ) 477 .build() 478 val writeWith = ClassName("com.squareup.parceler", "WriteWith") 479 val writeWithExternalClass = externalClass 480 .copy( 481 annotations = listOf( 482 AnnotationSpec 483 .builder(writeWith.plusParameter(externalClassParceler)) 484 .build(), 485 ), 486 ) 487 val typeLocalParceler = TypeSpec.classBuilder("MyClass") 488 .addAnnotation(parcelize) 489 .addProperty( 490 PropertySpec.builder("external", writeWithExternalClass) 491 .initializer("external") 492 .build(), 493 ) 494 .primaryConstructor( 495 FunSpec.constructorBuilder() 496 .addParameter("external", writeWithExternalClass) 497 .build(), 498 ) 499 .build() 500 val file = FileSpec.builder("com.squareup.parceler", "Test") 501 .addType(externalClassSpec) 502 .addType(externalClassParcelerSpec) 503 .addType(classLocalParceler) 504 .addType(propertyLocalParceler) 505 .addType(typeLocalParceler) 506 .build() 507 //language=kotlin 508 assertThat(file.toString()).isEqualTo( 509 """ 510 package com.squareup.parceler 511 512 import kotlin.Int 513 514 public class ExternalClass( 515 public val `value`: Int, 516 ) 517 518 public object ExternalClassParceler : Parceler<ExternalClass> { 519 override fun create(parcel: Parcel): ExternalClass = ExternalClass(parcel.readInt()) 520 521 override fun ExternalClass.write(parcel: Parcel, flags: Int) { 522 parcel.writeInt(value) 523 } 524 } 525 526 @Parcelize 527 @TypeParceler<ExternalClass, ExternalClassParceler> 528 public class MyClass( 529 public val `external`: ExternalClass, 530 ) 531 532 @Parcelize 533 public class MyClass( 534 @TypeParceler<ExternalClass, ExternalClassParceler> 535 public val `external`: ExternalClass, 536 ) 537 538 @Parcelize 539 public class MyClass( 540 public val `external`: @WriteWith<ExternalClassParceler> ExternalClass, 541 ) 542 543 """.trimIndent(), 544 ) 545 } 546 toStringnull547 private fun toString(annotationSpec: AnnotationSpec) = 548 toString(TypeSpec.classBuilder("Taco").addAnnotation(annotationSpec).build()) 549 550 private fun toString(typeSpec: TypeSpec) = 551 FileSpec.get("com.squareup.tacos", typeSpec).toString() 552 } 553