Skip to content
This repository was archived by the owner on Feb 26, 2023. It is now read-only.

Commit 4eba2d9

Browse files
Added the ability to put the @builder annotation on a constructor
1 parent b38850f commit 4eba2d9

File tree

5 files changed

+183
-64
lines changed

5 files changed

+183
-64
lines changed

build.gradle

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
21
buildscript {
32
ext.kotlin_version = '1.2.60'
43

@@ -12,9 +11,9 @@ buildscript {
1211
}
1312

1413

15-
allprojects{
14+
allprojects {
1615
apply plugin: "kotlin"
17-
16+
1817
repositories {
1918
mavenCentral()
2019
}

builder-annotation/src/main/kotlin/com/thinkinglogic/builder/annotation/Builder.kt

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,29 +5,29 @@ package com.thinkinglogic.builder.annotation
55
* {AnnotatedClassName}Builder class to be generated.
66
*/
77
@Retention(AnnotationRetention.SOURCE)
8-
@Target(AnnotationTarget.CLASS)
8+
@Target(AnnotationTarget.CLASS, AnnotationTarget.CONSTRUCTOR)
99
annotation class Builder
1010

1111
/**
1212
* Use this annotation to mark a collection or array as being allowed to contain null values,
1313
* As knowledge of the nullability is otherwise lost during annotation processing.
1414
*/
1515
@Retention(AnnotationRetention.SOURCE)
16-
@Target(AnnotationTarget.FIELD)
16+
@Target(AnnotationTarget.FIELD, AnnotationTarget.VALUE_PARAMETER)
1717
annotation class NullableType
1818

1919
/**
2020
* Use this annotation to mark a MutableList, MutableSet, MutableCollection, MutableMap, or MutableIterator,
2121
* as knowledge of their mutability is otherwise lost during annotation processing.
2222
*/
2323
@Retention(AnnotationRetention.SOURCE)
24-
@Target(AnnotationTarget.FIELD)
24+
@Target(AnnotationTarget.FIELD, AnnotationTarget.VALUE_PARAMETER)
2525
annotation class Mutable
2626

2727
/**
2828
* Use this annotation to provide a default value for the builder,
2929
* as knowledge of default values is otherwise lost during annotation processing.
3030
*/
3131
@Retention(AnnotationRetention.SOURCE)
32-
@Target(AnnotationTarget.FIELD)
32+
@Target(AnnotationTarget.FIELD, AnnotationTarget.VALUE_PARAMETER)
3333
annotation class DefaultValue (val value: String = "")

builder-processor/src/main/kotlin/com/thinkinglogic/builder/processor/BuilderProcessor.kt

Lines changed: 96 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,11 @@ import com.thinkinglogic.builder.annotation.Mutable
88
import com.thinkinglogic.builder.annotation.NullableType
99
import org.jetbrains.annotations.NotNull
1010
import java.io.File
11+
import java.util.*
1112
import java.util.stream.Collectors
1213
import javax.annotation.processing.*
1314
import javax.lang.model.SourceVersion
14-
import javax.lang.model.element.Element
15-
import javax.lang.model.element.TypeElement
16-
import javax.lang.model.element.VariableElement
15+
import javax.lang.model.element.*
1716
import javax.lang.model.type.ArrayType
1817
import javax.lang.model.type.DeclaredType
1918
import javax.lang.model.type.PrimitiveType
@@ -60,30 +59,36 @@ class BuilderProcessor : AbstractProcessor() {
6059
val sourceRootFile = File(generatedSourcesRoot)
6160
sourceRootFile.mkdir()
6261

63-
annotatedElements.forEach { annotatedClass ->
64-
if (annotatedClass !is TypeElement) {
65-
annotatedClass.errorMessage { "Invalid element type, expected a class" }
66-
return@forEach
62+
annotatedElements.forEach { annotatedElement ->
63+
when (annotatedElement.kind) {
64+
ElementKind.CLASS -> writeBuilderForClass(annotatedElement as TypeElement, sourceRootFile)
65+
ElementKind.CONSTRUCTOR -> writeBuilderForConstructor(annotatedElement as ExecutableElement, sourceRootFile)
66+
else -> annotatedElement.errorMessage { "Invalid element type, expected a class or constructor" }
6767
}
68-
69-
writeBuilder(annotatedClass, sourceRootFile)
7068
}
7169

7270
return false
7371
}
7472

75-
/**
76-
* Writes the source code to create a builder for [classToBuild] within the [sourceRoot] directory.
77-
*/
78-
private fun writeBuilder(classToBuild: TypeElement, sourceRoot: File) {
73+
/** Invokes [writeBuilder] to create a builder for the given [classElement]. */
74+
private fun writeBuilderForClass(classElement: TypeElement, sourceRootFile: File) {
75+
writeBuilder(classElement, classElement.fieldsForBuilder(), sourceRootFile)
76+
}
77+
78+
/** Invokes [writeBuilder] to create a builder for the given [constructor]. */
79+
private fun writeBuilderForConstructor(constructor: ExecutableElement, sourceRootFile: File) {
80+
writeBuilder(constructor.enclosingElement as TypeElement, constructor.parameters, sourceRootFile)
81+
}
82+
83+
/** Writes the source code to create a builder for [classToBuild] within the [sourceRoot] directory. */
84+
private fun writeBuilder(classToBuild: TypeElement, fields: List<VariableElement>, sourceRoot: File) {
7985
val packageName = processingEnv.elementUtils.getPackageOf(classToBuild).toString()
8086
val builderClassName = "${classToBuild.simpleName}Builder"
8187

8288
processingEnv.noteMessage { "Writing $packageName.$builderClassName" }
8389

8490
val builderSpec = TypeSpec.classBuilder(builderClassName)
8591
val builderClass = ClassName(packageName, builderClassName)
86-
val fields = classToBuild.fieldsForBuilder()
8792

8893
fields.forEach { field ->
8994
processingEnv.noteMessage { "Adding field: $field" }
@@ -100,9 +105,7 @@ class BuilderProcessor : AbstractProcessor() {
100105
.writeTo(sourceRoot)
101106
}
102107

103-
/**
104-
* Returns all fields in this type that also appear as a constructor parameter.
105-
*/
108+
/** Returns all fields in this type that also appear as a constructor parameter. */
106109
private fun TypeElement.fieldsForBuilder(): List<VariableElement> {
107110
val allMembers = processingEnv.elementUtils.getAllMembers(this)
108111
val fields = fieldsIn(allMembers)
@@ -114,9 +117,7 @@ class BuilderProcessor : AbstractProcessor() {
114117
return fields.filter { constructorParamNames.contains(it.simpleName.toString()) }
115118
}
116119

117-
/**
118-
* Creates a 'build()' function that will invoke a constructor for [returnType], passing [fields] as arguments and returning the new instance.
119-
*/
120+
/** Creates a 'build()' function that will invoke a constructor for [returnType], passing [fields] as arguments and returning the new instance. */
120121
private fun createBuildFunction(fields: List<Element>, returnType: TypeElement): FunSpec {
121122
val code = StringBuilder("$CHECK_REQUIRED_FIELDS_FUNCTION_NAME()")
122123
code.appendln().append("return ${returnType.simpleName}(")
@@ -139,13 +140,10 @@ class BuilderProcessor : AbstractProcessor() {
139140
.build()
140141
}
141142

142-
/**
143-
* Creates a function that will invoke [check] to confirm that each required field is populated.
144-
*/
143+
/** Creates a function that will invoke [check] to confirm that each required field is populated. */
145144
private fun createCheckRequiredFieldsFunction(fields: List<Element>): FunSpec {
146145
val code = StringBuilder()
147-
fields
148-
.filterNot { it.isNullable() }
146+
fields.filterNot { it.isNullable() }
149147
.forEach { field ->
150148
code.append(" check(${field.simpleName} != null, {\"${field.simpleName} must not be null\"})").appendln()
151149
}
@@ -156,20 +154,16 @@ class BuilderProcessor : AbstractProcessor() {
156154
.build()
157155
}
158156

159-
/**
160-
* Creates a property for the field identified by this element.
161-
*/
157+
/** Creates a property for the field identified by this element. */
162158
private fun Element.asProperty(): PropertySpec =
163159
PropertySpec.varBuilder(simpleName.toString(), asKotlinTypeName().asNullable(), KModifier.PRIVATE)
164-
.initializer("${defaultValue()}")
160+
.initializer(defaultValue())
165161
.build()
166162

167-
/**
168-
* Returns the correct default value for this element - the value of any [DefaultValue] annotation, or "null".
169-
*/
163+
/** Returns the correct default value for this element - the value of any [DefaultValue] annotation, or "null". */
170164
private fun Element.defaultValue(): String {
171165
return if (hasAnnotation(DefaultValue::class.java)) {
172-
val default = this.getAnnotation(DefaultValue::class.java).value
166+
val default = this.findAnnotation(DefaultValue::class.java).value
173167
// make sure that strings are wrapped in quotes
174168
return if (asType().toString() == "java.lang.String" && !default.startsWith("\"")) {
175169
"\"$default\""
@@ -181,9 +175,7 @@ class BuilderProcessor : AbstractProcessor() {
181175
}
182176
}
183177

184-
/**
185-
* Creates a function that sets the property identified by this element, and returns the [builder].
186-
*/
178+
/** Creates a function that sets the property identified by this element, and returns the [builder]. */
187179
private fun Element.asSetterFunctionReturning(builder: ClassName): FunSpec {
188180
val fieldType = asKotlinTypeName()
189181
val parameterClass = if (isNullable()) {
@@ -206,27 +198,17 @@ class BuilderProcessor : AbstractProcessor() {
206198
var typeName = asType().asKotlinTypeName()
207199
if (typeName is ParameterizedTypeName) {
208200
if (hasAnnotation(NullableType::class.java)
209-
&& verify(typeName.typeArguments.isNotEmpty(), "NullableType annotation should not be applied to a property without type arguments!")) {
201+
&& assert(typeName.typeArguments.isNotEmpty(), "NullableType annotation should not be applied to a property without type arguments!")) {
210202
typeName = typeName.withNullableType()
211203
}
212204
if (hasAnnotation(Mutable::class.java)
213-
&& verify(MUTABLE_COLLECTIONS.containsKey(typeName.rawType), "Mutable annotation should not be applied to non-mutable collections!")) {
205+
&& assert(MUTABLE_COLLECTIONS.containsKey(typeName.rawType), "Mutable annotation should not be applied to non-mutable collections!")) {
214206
typeName = typeName.asMutableCollection()
215207
}
216208
}
217209
return typeName
218210
}
219211

220-
/**
221-
* Returns the given [fact], logging an error message if it is not true.
222-
*/
223-
private fun Element.verify(fact: Boolean, message: String): Boolean {
224-
if (!fact) {
225-
this.errorMessage { message }
226-
}
227-
return fact
228-
}
229-
230212
/**
231213
* Converts this type to one containing nullable elements.
232214
*
@@ -251,12 +233,14 @@ class BuilderProcessor : AbstractProcessor() {
251233
val mutable = MUTABLE_COLLECTIONS[rawType]!!
252234
.parameterizedBy(*this.typeArguments.toTypedArray())
253235
.annotated(this.annotations)
254-
return if (nullable) { mutable.asNullable() } else { mutable }
236+
return if (nullable) {
237+
mutable.asNullable()
238+
} else {
239+
mutable
240+
}
255241
}
256242

257-
/**
258-
* Converts this TypeMirror to a [TypeName], ensuring that java types such as [java.lang.String] are converted to their Kotlin equivalent.
259-
*/
243+
/** Converts this TypeMirror to a [TypeName], ensuring that java types such as [java.lang.String] are converted to their Kotlin equivalent. */
260244
private fun TypeMirror.asKotlinTypeName(): TypeName {
261245
return when (this) {
262246
is PrimitiveType -> processingEnv.typeUtils.boxedClass(this as PrimitiveType?).asKotlinClassName()
@@ -279,9 +263,7 @@ class BuilderProcessor : AbstractProcessor() {
279263
}
280264
}
281265

282-
/**
283-
* Converts this element to a [ClassName], ensuring that java types such as [java.lang.String] are converted to their Kotlin equivalent.
284-
*/
266+
/** Converts this element to a [ClassName], ensuring that java types such as [java.lang.String] are converted to their Kotlin equivalent. */
285267
private fun TypeElement.asKotlinClassName(): ClassName {
286268
val className = asClassName()
287269
return try {
@@ -293,22 +275,79 @@ class BuilderProcessor : AbstractProcessor() {
293275
}
294276
}
295277

278+
/** Returns the [TypeElement] represented by this [TypeMirror]. */
296279
private fun TypeMirror.asTypeElement() = processingEnv.typeUtils.asElement(this) as TypeElement
297280

281+
/** Returns true as long as this [Element] is not a [PrimitiveType] and does not have the [NotNull] annotation. */
298282
private fun Element.isNullable(): Boolean {
299283
if (this.asType() is PrimitiveType) {
300284
return false
301285
}
302286
return !hasAnnotation(NotNull::class.java)
303287
}
304288

305-
private fun Element.hasAnnotation(annotationClass: Class<*>): Boolean {
289+
/**
290+
* Returns true if this element has the specified [annotation], or if the parent class has a matching constructor parameter with the annotation.
291+
* (This is necessary because builder annotations can be applied to both fields and constructor parameters - and constructor parameters take precedence.
292+
* Rather than require clients to specify, for instance, `@field:NullableType`, this method also checks for annotations of constructor parameters
293+
* when this element is a field).
294+
*/
295+
private fun Element.hasAnnotation(annotation: Class<*>): Boolean {
296+
return hasAnnotationDirectly(annotation) || hasAnnotationViaConstructorParameter(annotation)
297+
}
298+
299+
/** Return true if this element has the specified [annotation]. */
300+
private fun Element.hasAnnotationDirectly(annotation: Class<*>): Boolean {
306301
return this.annotationMirrors
307302
.map { it.annotationType.toString() }
308303
.toSet()
309-
.contains(annotationClass.name)
304+
.contains(annotation.name)
305+
}
306+
307+
/** Return true if there is a constructor parameter with the same name as this element that has the specified [annotation]. */
308+
private fun Element.hasAnnotationViaConstructorParameter(annotation: Class<*>): Boolean {
309+
val parameterAnnotations = getConstructorParameter()?.annotationMirrors ?: listOf()
310+
return parameterAnnotations
311+
.map { it.annotationType.toString() }
312+
.toSet()
313+
.contains(annotation.name)
314+
}
315+
316+
/** Returns the first constructor parameter with the same name as this element, if any such exists. */
317+
private fun Element.getConstructorParameter(): VariableElement? {
318+
val enclosingElement = this.enclosingElement
319+
return if (enclosingElement is TypeElement) {
320+
val allMembers = processingEnv.elementUtils.getAllMembers(enclosingElement)
321+
constructorsIn(allMembers)
322+
.flatMap { it.parameters }
323+
.firstOrNull { it.simpleName == this.simpleName }
324+
} else {
325+
null
326+
}
327+
}
328+
329+
/**
330+
* Returns the given annotation, retrieved from this element directly, or from the corresponding constructor parameter.
331+
*
332+
* @throws NullPointerException if no such annotation can be found - use [hasAnnotation] before calling this method.
333+
*/
334+
private fun <A : Annotation> Element.findAnnotation(annotation: Class<A>): A {
335+
return if (hasAnnotationDirectly(annotation)) {
336+
getAnnotation(annotation)
337+
} else {
338+
getConstructorParameter()!!.getAnnotation(annotation)
339+
}
340+
}
341+
342+
/** Returns the given [assertion], logging an error message if it is not true. */
343+
private fun Element.assert(assertion: Boolean, message: String): Boolean {
344+
if (!assertion) {
345+
this.errorMessage { message }
346+
}
347+
return assertion
310348
}
311349

350+
/** Prints an error message using this element as a position hint. */
312351
private fun Element.errorMessage(message: () -> String) {
313352
processingEnv.messager.printMessage(ERROR, message(), this)
314353
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
package com.thinkinglogic.example
2+
3+
import com.thinkinglogic.builder.annotation.Builder
4+
import com.thinkinglogic.builder.annotation.DefaultValue
5+
6+
class ClassWithConstructorParameters
7+
@Builder
8+
constructor(
9+
forename: String,
10+
@DefaultValue("Anon") surname: String = "Anon",
11+
val otherName: String?
12+
) {
13+
val fullName = "$forename $surname"
14+
15+
override fun equals(other: Any?): Boolean {
16+
if (this === other) return true
17+
if (javaClass != other?.javaClass) return false
18+
19+
other as ClassWithConstructorParameters
20+
21+
if (otherName != other.otherName) return false
22+
if (fullName != other.fullName) return false
23+
24+
return true
25+
}
26+
27+
override fun hashCode(): Int {
28+
var result = otherName?.hashCode() ?: 0
29+
result = 31 * result + fullName.hashCode()
30+
return result
31+
}
32+
}

0 commit comments

Comments
 (0)