• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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