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 |import kotlin.Unit 322 | 323 |public fun test(): Unit { 324 | @Suppress("Things") 325 | val annotatedString = "AnnotatedString" 326 |} 327 """.trimMargin(), 328 ) 329 } 330 functionOnlyLiteralAnnotationnull331 @Test fun functionOnlyLiteralAnnotation() { 332 val annotation = AnnotationSpec 333 .builder(ClassName.bestGuess("Suppress")) 334 .addMember("%S", "UNCHECKED_CAST") 335 .build() 336 val funSpec = FunSpec.builder("operation") 337 .addStatement("%L", annotation) 338 .build() 339 340 assertThat(funSpec.toString().trim()).isEqualTo( 341 """ 342 |public fun operation(): kotlin.Unit { 343 | @Suppress("UNCHECKED_CAST") 344 |} 345 """.trimMargin(), 346 ) 347 } 348 getOnValueArrayTypeMirrorShouldNameValueArgnull349 @Test fun getOnValueArrayTypeMirrorShouldNameValueArg() { 350 val myClazz = compilation.elements 351 .getTypeElement(JavaClassWithArrayValueAnnotation::class.java.canonicalName) 352 val classBuilder = TypeSpec.classBuilder("Result") 353 354 myClazz.annotationMirrors.map { AnnotationSpec.get(it) } 355 .forEach { 356 classBuilder.addAnnotation(it) 357 } 358 359 assertThat(toString(classBuilder.build())).isEqualTo( 360 """ 361 |package com.squareup.tacos 362 | 363 |import com.squareup.kotlinpoet.JavaClassWithArrayValueAnnotation 364 |import java.lang.Boolean 365 |import java.lang.Object 366 | 367 |@JavaClassWithArrayValueAnnotation.AnnotationWithArrayValue(value = arrayOf(Object::class, 368 | Boolean::class)) 369 |public class Result 370 | 371 """.trimMargin(), 372 ) 373 } 374 getOnVarargMirrorShouldNameValueArgnull375 @Test fun getOnVarargMirrorShouldNameValueArg() { 376 val myClazz = compilation.elements 377 .getTypeElement(KotlinClassWithVarargAnnotation::class.java.canonicalName) 378 val classBuilder = TypeSpec.classBuilder("Result") 379 380 myClazz.annotationMirrors.map { AnnotationSpec.get(it) } 381 .filter { 382 val typeName = it.typeName 383 return@filter typeName is ClassName && typeName.simpleName == "AnnotationWithArrayValue" 384 } 385 .forEach { 386 classBuilder.addAnnotation(it) 387 } 388 389 assertThat(toString(classBuilder.build()).trim()).isEqualTo( 390 """ 391 |package com.squareup.tacos 392 | 393 |import com.squareup.kotlinpoet.AnnotationSpecTest 394 |import java.lang.Object 395 |import kotlin.Boolean 396 | 397 |@AnnotationSpecTest.AnnotationWithArrayValue(value = arrayOf(Object::class, Boolean::class)) 398 |public class Result 399 """.trimMargin(), 400 ) 401 } 402 getOnValueArrayTypeAnnotationShouldNameValueArgnull403 @Test fun getOnValueArrayTypeAnnotationShouldNameValueArg() { 404 val annotation = JavaClassWithArrayValueAnnotation::class.java.getAnnotation( 405 JavaClassWithArrayValueAnnotation.AnnotationWithArrayValue::class.java, 406 ) 407 val classBuilder = TypeSpec.classBuilder("Result") 408 .addAnnotation(AnnotationSpec.get(annotation)) 409 410 assertThat(toString(classBuilder.build()).trim()).isEqualTo( 411 """ 412 |package com.squareup.tacos 413 | 414 |import com.squareup.kotlinpoet.JavaClassWithArrayValueAnnotation 415 |import java.lang.Boolean 416 |import java.lang.Object 417 | 418 |@JavaClassWithArrayValueAnnotation.AnnotationWithArrayValue(value = arrayOf(Object::class, 419 | Boolean::class)) 420 |public class Result 421 """.trimMargin(), 422 ) 423 } 424 getOnVarargAnnotationShouldNameValueArgnull425 @Test fun getOnVarargAnnotationShouldNameValueArg() { 426 val annotation = KotlinClassWithVarargAnnotation::class.java 427 .getAnnotation(AnnotationWithArrayValue::class.java) 428 val classBuilder = TypeSpec.classBuilder("Result") 429 .addAnnotation(AnnotationSpec.get(annotation)) 430 431 assertThat(toString(classBuilder.build()).trim()).isEqualTo( 432 """ 433 |package com.squareup.tacos 434 | 435 |import com.squareup.kotlinpoet.AnnotationSpecTest 436 |import java.lang.Object 437 |import kotlin.Boolean 438 | 439 |@AnnotationSpecTest.AnnotationWithArrayValue(value = arrayOf(Object::class, Boolean::class)) 440 |public class Result 441 """.trimMargin(), 442 ) 443 } 444 445 @AnnotationWithArrayValue(Any::class, Boolean::class) 446 class KotlinClassWithVarargAnnotation 447 448 @Retention(AnnotationRetention.RUNTIME) 449 internal annotation class AnnotationWithArrayValue(vararg val value: KClass<*>) 450 annotationsWithTypeParametersnull451 @Test fun annotationsWithTypeParameters() { 452 // Example from https://kotlinlang.org/docs/tutorials/android-plugin.html 453 val externalClass = ClassName("com.squareup.parceler", "ExternalClass") 454 val externalClassSpec = TypeSpec.classBuilder(externalClass) 455 .addProperty( 456 PropertySpec.builder("value", Int::class) 457 .initializer("value") 458 .build(), 459 ) 460 .primaryConstructor( 461 FunSpec.constructorBuilder() 462 .addParameter("value", Int::class) 463 .build(), 464 ) 465 .build() 466 val externalClassParceler = ClassName("com.squareup.parceler", "ExternalClassParceler") 467 val parcel = ClassName("com.squareup.parceler", "Parcel") 468 val externalClassParcelerSpec = TypeSpec.objectBuilder(externalClassParceler) 469 .addSuperinterface( 470 ClassName("com.squareup.parceler", "Parceler") 471 .parameterizedBy(externalClass), 472 ) 473 .addFunction( 474 FunSpec.builder("create") 475 .addModifiers(OVERRIDE) 476 .addParameter("parcel", parcel) 477 .addStatement("return %T(parcel.readInt())", externalClass) 478 .build(), 479 ) 480 .addFunction( 481 FunSpec.builder("write") 482 .addModifiers(OVERRIDE) 483 .receiver(externalClass) 484 .addParameter("parcel", parcel) 485 .addParameter("flags", Int::class) 486 .addStatement("parcel.writeInt(value)") 487 .build(), 488 ) 489 .build() 490 val parcelize = ClassName("com.squareup.parceler", "Parcelize") 491 val typeParceler = ClassName("com.squareup.parceler", "TypeParceler") 492 val typeParcelerAnnotation = AnnotationSpec.builder( 493 typeParceler 494 .plusParameter(externalClass) 495 .plusParameter(externalClassParceler), 496 ) 497 .build() 498 val classLocalParceler = TypeSpec.classBuilder("MyClass") 499 .addAnnotation(parcelize) 500 .addAnnotation(typeParcelerAnnotation) 501 .addProperty( 502 PropertySpec.builder("external", externalClass) 503 .initializer("external") 504 .build(), 505 ) 506 .primaryConstructor( 507 FunSpec.constructorBuilder() 508 .addParameter("external", externalClass) 509 .build(), 510 ) 511 .build() 512 val propertyLocalParceler = TypeSpec.classBuilder("MyClass") 513 .addAnnotation(parcelize) 514 .addProperty( 515 PropertySpec.builder("external", externalClass) 516 .addAnnotation(typeParcelerAnnotation) 517 .initializer("external") 518 .build(), 519 ) 520 .primaryConstructor( 521 FunSpec.constructorBuilder() 522 .addParameter("external", externalClass) 523 .build(), 524 ) 525 .build() 526 val writeWith = ClassName("com.squareup.parceler", "WriteWith") 527 val writeWithExternalClass = externalClass 528 .copy( 529 annotations = listOf( 530 AnnotationSpec 531 .builder(writeWith.plusParameter(externalClassParceler)) 532 .build(), 533 ), 534 ) 535 val typeLocalParceler = TypeSpec.classBuilder("MyClass") 536 .addAnnotation(parcelize) 537 .addProperty( 538 PropertySpec.builder("external", writeWithExternalClass) 539 .initializer("external") 540 .build(), 541 ) 542 .primaryConstructor( 543 FunSpec.constructorBuilder() 544 .addParameter("external", writeWithExternalClass) 545 .build(), 546 ) 547 .build() 548 val file = FileSpec.builder("com.squareup.parceler", "Test") 549 .addType(externalClassSpec) 550 .addType(externalClassParcelerSpec) 551 .addType(classLocalParceler) 552 .addType(propertyLocalParceler) 553 .addType(typeLocalParceler) 554 .build() 555 //language=kotlin 556 assertThat(file.toString()).isEqualTo( 557 """ 558 package com.squareup.parceler 559 560 import kotlin.Int 561 import kotlin.Unit 562 563 public class ExternalClass( 564 public val `value`: Int, 565 ) 566 567 public object ExternalClassParceler : Parceler<ExternalClass> { 568 public override fun create(parcel: Parcel) = ExternalClass(parcel.readInt()) 569 570 public override fun ExternalClass.write(parcel: Parcel, flags: Int): Unit { 571 parcel.writeInt(value) 572 } 573 } 574 575 @Parcelize 576 @TypeParceler<ExternalClass, ExternalClassParceler> 577 public class MyClass( 578 public val `external`: ExternalClass, 579 ) 580 581 @Parcelize 582 public class MyClass( 583 @TypeParceler<ExternalClass, ExternalClassParceler> 584 public val `external`: ExternalClass, 585 ) 586 587 @Parcelize 588 public class MyClass( 589 public val `external`: @WriteWith<ExternalClassParceler> ExternalClass, 590 ) 591 592 """.trimIndent(), 593 ) 594 } 595 toStringnull596 private fun toString(annotationSpec: AnnotationSpec) = 597 toString(TypeSpec.classBuilder("Taco").addAnnotation(annotationSpec).build()) 598 599 private fun toString(typeSpec: TypeSpec) = 600 FileSpec.get("com.squareup.tacos", typeSpec).toString() 601 } 602