1 /*
<lambda>null2  * 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 @file:JvmName("CodeBlocks")
17 
18 package com.squareup.kotlinpoet
19 
20 import java.lang.reflect.Type
21 import java.text.DecimalFormat
22 import java.text.DecimalFormatSymbols
23 import javax.lang.model.element.Element
24 import javax.lang.model.type.TypeMirror
25 import kotlin.reflect.KClass
26 
27 /**
28  * A fragment of a .kt file, potentially containing declarations, statements, and documentation.
29  * Code blocks are not necessarily well-formed Kotlin code, and are not validated. This class
30  * assumes kotlinc will check correctness later!
31  *
32  * Code blocks support placeholders like [java.text.Format]. This class primarily uses a percent
33  * sign `%` but has its own set of permitted placeholders:
34  *
35  *  * `%L` emits a *literal* value with no escaping. Arguments for literals may be strings,
36  *    primitives, [type declarations][TypeSpec], [annotations][AnnotationSpec] and even other code
37  *    blocks.
38  *  * `%N` emits a *name*, using name collision avoidance where necessary. Arguments for names may
39  *    be strings (actually any [character sequence][CharSequence]), [parameters][ParameterSpec],
40  *    [properties][PropertySpec], [functions][FunSpec], and [types][TypeSpec].
41  *  * `%S` escapes the value as a *string*, wraps it with double quotes, and emits that. For
42  *    example, `6" sandwich` is emitted `"6\" sandwich"`. `%S` will also escape all dollar signs
43  *    (`$`), use `%P` for string templates.
44  *  * `%P` - Similar to `%S`, but doesn't escape dollar signs (`$`) to allow creation of string
45  *    templates. If the string contains dollar signs that should be escaped - use `%S`.
46  *  * `%T` emits a *type* reference. Types will be imported if possible. Arguments for types may be
47  *    [classes][Class].
48  *  * `%M` emits a *member* reference. A member is either a function or a property. If the member is
49  *    importable, e.g. it's a top-level function or a property declared inside an object, the import
50  *    will be resolved if possible. Arguments for members must be of type [MemberName].
51  *  * `%%` emits a percent sign.
52  *  * `·` emits a space that never wraps. KotlinPoet prefers to wrap lines longer than 100 columns.
53  *    It does this by replacing normal spaces with a newline and indent. Note that spaces in strings
54  *    are never wrapped.
55  *  * `⇥` increases the indentation level.
56  *  * `⇤` decreases the indentation level.
57  *  * `«` begins a statement. For multiline statements, every line after the first line is
58  *    double-indented.
59  *  * `»` ends a statement.
60  */
61 public class CodeBlock private constructor(
62   internal val formatParts: List<String>,
63   internal val args: List<Any?>,
64 ) {
65   /** A heterogeneous list containing string literals and value placeholders.  */
66 
67   public fun isEmpty(): Boolean = formatParts.isEmpty()
68 
69   public fun isNotEmpty(): Boolean = !isEmpty()
70 
71   /**
72    * Returns a code block with `prefix` stripped off, or null if this code block doesn't start with
73    * `prefix`.
74    *
75    * This is a pretty basic implementation that might not cover cases like mismatched whitespace. We
76    * could offer something more lenient if necessary.
77    */
78   internal fun withoutPrefix(prefix: CodeBlock): CodeBlock? {
79     if (formatParts.size < prefix.formatParts.size) return null
80     if (args.size < prefix.args.size) return null
81 
82     var prefixArgCount = 0
83     var firstFormatPart: String? = null
84 
85     // Walk through the formatParts of prefix to confirm that it's a of this.
86     prefix.formatParts.forEachIndexed { index, formatPart ->
87       if (formatParts[index] != formatPart) {
88         // We've found a format part that doesn't match. If this is the very last format part check
89         // for a string prefix match. If that doesn't match, we're done.
90         if (index == prefix.formatParts.size - 1 && formatParts[index].startsWith(formatPart)) {
91           firstFormatPart = formatParts[index].substring(formatPart.length)
92         } else {
93           return null
94         }
95       }
96 
97       // If the matching format part has an argument, check that too.
98       if (formatPart.startsWith("%") && !formatPart[1].isMultiCharNoArgPlaceholder) {
99         if (args[prefixArgCount] != prefix.args[prefixArgCount]) {
100           return null // Argument doesn't match.
101         }
102         prefixArgCount++
103       }
104     }
105 
106     // We found a prefix. Prepare the suffix as a result.
107     val resultFormatParts = ArrayList<String>()
108     firstFormatPart?.let {
109       resultFormatParts.add(it)
110     }
111     for (i in prefix.formatParts.size until formatParts.size) {
112       resultFormatParts.add(formatParts[i])
113     }
114 
115     val resultArgs = ArrayList<Any?>()
116     for (i in prefix.args.size until args.size) {
117       resultArgs.add(args[i])
118     }
119 
120     return CodeBlock(resultFormatParts, resultArgs)
121   }
122 
123   /**
124    * Returns a copy of the code block without leading and trailing no-arg placeholders
125    * (`⇥`, `⇤`, `«`, `»`).
126    */
127   internal fun trim(): CodeBlock {
128     var start = 0
129     var end = formatParts.size
130     while (start < end && formatParts[start] in NO_ARG_PLACEHOLDERS) {
131       start++
132     }
133     while (start < end && formatParts[end - 1] in NO_ARG_PLACEHOLDERS) {
134       end--
135     }
136     return when {
137       start > 0 || end < formatParts.size -> CodeBlock(formatParts.subList(start, end), args)
138       else -> this
139     }
140   }
141 
142   /**
143    * Returns a copy of the code block with selected format parts replaced, similar to
144    * [java.lang.String.replaceAll].
145    *
146    * **Warning!** This method leaves the arguments list unchanged. Take care when replacing
147    * placeholders with arguments, such as `%L`, as it can result in a code block, where
148    * placeholders don't match their arguments.
149    */
150   internal fun replaceAll(oldValue: String, newValue: String) =
151     CodeBlock(formatParts.map { it.replace(oldValue, newValue) }, args)
152 
153   internal fun hasStatements() = formatParts.any { "«" in it }
154 
155   override fun equals(other: Any?): Boolean {
156     if (this === other) return true
157     if (other == null) return false
158     if (javaClass != other.javaClass) return false
159     return toString() == other.toString()
160   }
161 
162   override fun hashCode(): Int = toString().hashCode()
163 
164   override fun toString(): String = buildCodeString { emitCode(this@CodeBlock) }
165 
166   internal fun toString(codeWriter: CodeWriter): String = buildCodeString(codeWriter) {
167     emitCode(this@CodeBlock)
168   }
169 
170   public fun toBuilder(): Builder {
171     val builder = Builder()
172     builder.formatParts += formatParts
173     builder.args.addAll(args)
174     return builder
175   }
176 
177   public class Builder {
178     internal val formatParts = mutableListOf<String>()
179     internal val args = mutableListOf<Any?>()
180 
181     public fun isEmpty(): Boolean = formatParts.isEmpty()
182 
183     public fun isNotEmpty(): Boolean = !isEmpty()
184 
185     /**
186      * Adds code using named arguments.
187      *
188      * Named arguments specify their name after the '%' followed by : and the corresponding type
189      * character. Argument names consist of characters in `a-z, A-Z, 0-9, and _` and must start
190      * with a lowercase character.
191      *
192      * For example, to refer to the type [java.lang.Integer] with the argument name `clazz` use a
193      * format string containing `%clazz:T` and include the key `clazz` with value
194      * `java.lang.Integer.class` in the argument map.
195      */
196     public fun addNamed(format: String, arguments: Map<String, *>): Builder = apply {
197       var p = 0
198 
199       for (argument in arguments.keys) {
200         require(LOWERCASE matches argument) {
201           "argument '$argument' must start with a lowercase character"
202         }
203       }
204 
205       while (p < format.length) {
206         val nextP = format.nextPotentialPlaceholderPosition(startIndex = p)
207         if (nextP == -1) {
208           formatParts += format.substring(p, format.length)
209           break
210         }
211 
212         if (p != nextP) {
213           formatParts += format.substring(p, nextP)
214           p = nextP
215         }
216 
217         var matchResult: MatchResult? = null
218         val colon = format.indexOf(':', p)
219         if (colon != -1) {
220           val endIndex = (colon + 2).coerceAtMost(format.length)
221           matchResult = NAMED_ARGUMENT.matchEntire(format.substring(p, endIndex))
222         }
223         if (matchResult != null) {
224           val argumentName = matchResult.groupValues[ARG_NAME]
225           require(arguments.containsKey(argumentName)) {
226             "Missing named argument for %$argumentName"
227           }
228           val formatChar = matchResult.groupValues[TYPE_NAME].first()
229           addArgument(format, formatChar, arguments[argumentName])
230           formatParts += "%$formatChar"
231           p += matchResult.range.last + 1
232         } else if (format[p].isSingleCharNoArgPlaceholder) {
233           formatParts += format.substring(p, p + 1)
234           p++
235         } else {
236           require(p < format.length - 1) { "dangling % at end" }
237           require(format[p + 1].isMultiCharNoArgPlaceholder) {
238             "unknown format %${format[p + 1]} at ${p + 1} in '$format'"
239           }
240           formatParts += format.substring(p, p + 2)
241           p += 2
242         }
243       }
244     }
245 
246     /**
247      * Add code with positional or relative arguments.
248      *
249      * Relative arguments map 1:1 with the placeholders in the format string.
250      *
251      * Positional arguments use an index after the placeholder to identify which argument index
252      * to use. For example, for a literal to reference the 3rd argument: "%3L" (1 based index)
253      *
254      * Mixing relative and positional arguments in a call to add is invalid and will result in an
255      * error.
256      */
257     public fun add(format: String, vararg args: Any?): Builder = apply {
258       var hasRelative = false
259       var hasIndexed = false
260 
261       var relativeParameterCount = 0
262       val indexedParameterCount = IntArray(args.size)
263 
264       var p = 0
265       while (p < format.length) {
266         if (format[p].isSingleCharNoArgPlaceholder) {
267           formatParts += format[p].toString()
268           p++
269           continue
270         }
271 
272         if (format[p] != '%') {
273           var nextP = format.nextPotentialPlaceholderPosition(startIndex = p + 1)
274           if (nextP == -1) nextP = format.length
275           formatParts += format.substring(p, nextP)
276           p = nextP
277           continue
278         }
279 
280         p++ // '%'.
281 
282         // Consume zero or more digits, leaving 'c' as the first non-digit char after the '%'.
283         val indexStart = p
284         var c: Char
285         do {
286           require(p < format.length) { "dangling format characters in '$format'" }
287           c = format[p++]
288         } while (c in '0'..'9')
289         val indexEnd = p - 1
290 
291         // If 'c' doesn't take an argument, we're done.
292         if (c.isMultiCharNoArgPlaceholder) {
293           require(indexStart == indexEnd) { "%% may not have an index" }
294           formatParts += "%$c"
295           continue
296         }
297 
298         // Find either the indexed argument, or the relative argument. (0-based).
299         val index: Int
300         if (indexStart < indexEnd) {
301           index = Integer.parseInt(format.substring(indexStart, indexEnd)) - 1
302           hasIndexed = true
303           if (args.isNotEmpty()) {
304             indexedParameterCount[index % args.size]++ // modulo is needed, checked below anyway
305           }
306         } else {
307           index = relativeParameterCount
308           hasRelative = true
309           relativeParameterCount++
310         }
311 
312         require(index >= 0 && index < args.size) {
313           "index ${index + 1} for '${format.substring(
314             indexStart - 1,
315             indexEnd + 1,
316           )}' not in range (received ${args.size} arguments)"
317         }
318         require(!hasIndexed || !hasRelative) { "cannot mix indexed and positional parameters" }
319 
320         addArgument(format, c, args[index])
321 
322         formatParts += "%$c"
323       }
324 
325       if (hasRelative) {
326         require(relativeParameterCount >= args.size) {
327           "unused arguments: expected $relativeParameterCount, received ${args.size}"
328         }
329       }
330       if (hasIndexed) {
331         val unused = mutableListOf<String>()
332         for (i in args.indices) {
333           if (indexedParameterCount[i] == 0) {
334             unused += "%" + (i + 1)
335           }
336         }
337         val s = if (unused.size == 1) "" else "s"
338         require(unused.isEmpty()) { "unused argument$s: ${unused.joinToString(", ")}" }
339       }
340     }
341 
342     private fun addArgument(format: String, c: Char, arg: Any?) {
343       when (c) {
344         'N' -> this.args += argToName(arg).escapeIfNecessary()
345         'L' -> this.args += argToLiteral(arg)
346         'S' -> this.args += argToString(arg)
347         'P' -> this.args += if (arg is CodeBlock) arg else argToString(arg)
348         'T' -> this.args += argToType(arg)
349         'M' -> this.args += arg
350         else -> throw IllegalArgumentException(
351           String.format("invalid format string: '%s'", format),
352         )
353       }
354     }
355 
356     private fun argToName(o: Any?) = when (o) {
357       is CharSequence -> o.toString()
358       is ParameterSpec -> o.name
359       is PropertySpec -> o.name
360       is FunSpec -> o.name
361       is TypeSpec -> o.name!!
362       is MemberName -> o.simpleName
363       else -> throw IllegalArgumentException("expected name but was $o")
364     }
365 
366     private fun argToLiteral(o: Any?) = if (o is Number) formatNumericValue(o) else o
367 
368     private fun argToString(o: Any?) = o?.toString()
369 
370     private fun formatNumericValue(o: Number): Any? {
371       val format = DecimalFormatSymbols().apply {
372         decimalSeparator = '.'
373         groupingSeparator = '_'
374       }
375 
376       val precision = if (o is Float || o is Double) o.toString().split(".").last().length else 0
377 
378       val pattern = when (o) {
379         is Float, is Double -> "###,##0.0" + "#".repeat(precision - 1)
380         else -> "###,##0"
381       }
382 
383       return DecimalFormat(pattern, format).format(o)
384     }
385 
386     private fun logDeprecationWarning(o: Any) {
387       println(
388         "Deprecation warning: converting $o to TypeName. Conversion of TypeMirror and" +
389           " TypeElement is deprecated in KotlinPoet, use kotlin-metadata APIs instead.",
390       )
391     }
392 
393     private fun argToType(o: Any?) = when (o) {
394       is TypeName -> o
395       is TypeMirror -> {
396         logDeprecationWarning(o)
397         o.asTypeName()
398       }
399       is Element -> {
400         logDeprecationWarning(o)
401         o.asType().asTypeName()
402       }
403       is Type -> o.asTypeName()
404       is KClass<*> -> o.asTypeName()
405       else -> throw IllegalArgumentException("expected type but was $o")
406     }
407 
408     /**
409      * @param controlFlow the control flow construct and its code, such as `if (foo == 5)`.
410      *     Shouldn't contain newline characters. Can contain opening braces, e.g.
411      *     `beginControlFlow("list.forEach { element ->")`. If there's no opening brace at the end
412      *     of the string, it will be added.
413      */
414     public fun beginControlFlow(controlFlow: String, vararg args: Any?): Builder = apply {
415       add(controlFlow.withOpeningBrace(), *args)
416       indent()
417     }
418 
419     private fun String.withOpeningBrace(): String {
420       for (i in length - 1 downTo 0) {
421         if (this[i] == '{') {
422           return "$this\n"
423         } else if (this[i] == '}') {
424           break
425         }
426       }
427       return "$this·{\n"
428     }
429 
430     /**
431      * @param controlFlow the control flow construct and its code, such as "else if (foo == 10)".
432      *     Shouldn't contain braces or newline characters.
433      */
434     public fun nextControlFlow(controlFlow: String, vararg args: Any?): Builder = apply {
435       unindent()
436       add("}·$controlFlow·{\n", *args)
437       indent()
438     }
439 
440     public fun endControlFlow(): Builder = apply {
441       unindent()
442       add("}\n")
443     }
444 
445     public fun addStatement(format: String, vararg args: Any?): Builder = apply {
446       add("«")
447       add(format, *args)
448       add("\n»")
449     }
450 
451     public fun add(codeBlock: CodeBlock): Builder = apply {
452       formatParts += codeBlock.formatParts
453       args.addAll(codeBlock.args)
454     }
455 
456     public fun indent(): Builder = apply {
457       formatParts += "⇥"
458     }
459 
460     public fun unindent(): Builder = apply {
461       formatParts += "⇤"
462     }
463 
464     public fun clear(): Builder = apply {
465       formatParts.clear()
466       args.clear()
467     }
468 
469     public fun build(): CodeBlock = CodeBlock(formatParts.toImmutableList(), args.toImmutableList())
470   }
471 
472   public companion object {
473     private val NAMED_ARGUMENT = Regex("%([\\w_]+):([\\w]).*")
474     private val LOWERCASE = Regex("[a-z]+[\\w_]*")
475     private const val ARG_NAME = 1
476     private const val TYPE_NAME = 2
477     private val NO_ARG_PLACEHOLDERS = setOf("⇥", "⇤", "«", "»")
478     internal val EMPTY = CodeBlock(emptyList(), emptyList())
479 
480     @JvmStatic public fun of(format: String, vararg args: Any?): CodeBlock =
481       Builder().add(format, *args).build()
482 
483     @JvmStatic public fun builder(): Builder = Builder()
484 
485     internal val Char.isMultiCharNoArgPlaceholder get() = this == '%'
486     internal val Char.isSingleCharNoArgPlaceholder get() = isOneOf('⇥', '⇤', '«', '»')
487     internal val String.isPlaceholder
488       get() = (length == 1 && first().isSingleCharNoArgPlaceholder) ||
489         (length == 2 && first().isMultiCharNoArgPlaceholder)
490 
491     internal fun String.nextPotentialPlaceholderPosition(startIndex: Int) =
492       indexOfAny(charArrayOf('%', '«', '»', '⇥', '⇤'), startIndex)
493   }
494 }
495 
496 @JvmOverloads
joinToCodenull497 public fun Collection<CodeBlock>.joinToCode(
498   separator: CharSequence = ", ",
499   prefix: CharSequence = "",
500   suffix: CharSequence = "",
501 ): CodeBlock {
502   val blocks = toTypedArray()
503   val placeholders = Array(blocks.size) { "%L" }
504   return CodeBlock.of(placeholders.joinToString(separator, prefix, suffix), *blocks)
505 }
506 
507 /**
508  * Builds new [CodeBlock] by populating newly created [CodeBlock.Builder] using provided
509  * [builderAction] and then converting it to [CodeBlock].
510  */
buildCodeBlocknull511 public inline fun buildCodeBlock(builderAction: CodeBlock.Builder.() -> Unit): CodeBlock {
512   return CodeBlock.builder().apply(builderAction).build()
513 }
514 
515 /**
516  * Calls [CodeBlock.Builder.indent] then executes the provided [builderAction] on the
517  * [CodeBlock.Builder] and then executes [CodeBlock.Builder.unindent] before returning the
518  * original [CodeBlock.Builder].
519  */
withIndentnull520 public inline fun CodeBlock.Builder.withIndent(builderAction: CodeBlock.Builder.() -> Unit): CodeBlock.Builder {
521   return indent().also(builderAction).unindent()
522 }
523