• 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       |
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