• 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.squareup.kotlinpoet.CodeBlock.Companion.isPlaceholder
19 import java.util.Collections
20 
21 internal object NullAppendable : Appendable {
appendnull22   override fun append(charSequence: CharSequence) = this
23   override fun append(charSequence: CharSequence, start: Int, end: Int) = this
24   override fun append(c: Char) = this
25 }
26 
27 internal fun <K, V> Map<K, V>.toImmutableMap(): Map<K, V> =
28   Collections.unmodifiableMap(LinkedHashMap(this))
29 
30 internal fun <T> Collection<T>.toImmutableList(): List<T> =
31   Collections.unmodifiableList(ArrayList(this))
32 
33 internal fun <T> Collection<T>.toImmutableSet(): Set<T> =
34   Collections.unmodifiableSet(LinkedHashSet(this))
35 
36 internal inline fun <reified T : Enum<T>> Collection<T>.toEnumSet(): Set<T> =
37   enumValues<T>().filterTo(mutableSetOf(), this::contains)
38 
39 internal fun requireNoneOrOneOf(modifiers: Set<KModifier>, vararg mutuallyExclusive: KModifier) {
40   val count = mutuallyExclusive.count(modifiers::contains)
41   require(count <= 1) {
42     "modifiers $modifiers must contain none or only one of ${mutuallyExclusive.contentToString()}"
43   }
44 }
45 
requireNoneOfnull46 internal fun requireNoneOf(modifiers: Set<KModifier>, vararg forbidden: KModifier) {
47   require(forbidden.none(modifiers::contains)) {
48     "modifiers $modifiers must contain none of ${forbidden.contentToString()}"
49   }
50 }
51 
isOneOfnull52 internal fun <T> T.isOneOf(t1: T, t2: T, t3: T? = null, t4: T? = null, t5: T? = null, t6: T? = null) =
53   this == t1 || this == t2 || this == t3 || this == t4 || this == t5 || this == t6
54 
55 internal fun <T> Collection<T>.containsAnyOf(vararg t: T) = t.any(this::contains)
56 
57 // see https://docs.oracle.com/javase/specs/jls/se7/html/jls-3.html#jls-3.10.6
58 internal fun characterLiteralWithoutSingleQuotes(c: Char) = when {
59   c == '\b' -> "\\b" // \u0008: backspace (BS)
60   c == '\t' -> "\\t" // \u0009: horizontal tab (HT)
61   c == '\n' -> "\\n" // \u000a: linefeed (LF)
62   c == '\r' -> "\\r" // \u000d: carriage return (CR)
63   c == '\"' -> "\"" // \u0022: double quote (")
64   c == '\'' -> "\\'" // \u0027: single quote (')
65   c == '\\' -> "\\\\" // \u005c: backslash (\)
66   c.isIsoControl -> String.format("\\u%04x", c.code)
67   else -> c.toString()
68 }
69 
<lambda>null70 internal fun escapeCharacterLiterals(s: String) = buildString {
71   for (c in s) append(characterLiteralWithoutSingleQuotes(c))
72 }
73 
74 private val Char.isIsoControl: Boolean
75   get() {
76     return this in '\u0000'..'\u001F' || this in '\u007F'..'\u009F'
77   }
78 
79 /** Returns the string literal representing `value`, including wrapping double quotes.  */
stringLiteralWithQuotesnull80 internal fun stringLiteralWithQuotes(
81   value: String,
82   isInsideRawString: Boolean = false,
83   isConstantContext: Boolean = false,
84 ): String {
85   if (!isConstantContext && '\n' in value) {
86     val result = StringBuilder(value.length + 32)
87     result.append("\"\"\"\n|")
88     var i = 0
89     while (i < value.length) {
90       val c = value[i]
91       if (value.regionMatches(i, "\"\"\"", 0, 3)) {
92         // Don't inadvertently end the raw string too early
93         result.append("\"\"\${'\"'}")
94         i += 2
95       } else if (c == '\n') {
96         // Add a '|' after newlines. This pipe will be removed by trimMargin().
97         result.append("\n|")
98       } else if (c == '$' && !isInsideRawString) {
99         // Escape '$' symbols with ${'$'}.
100         result.append("\${\'\$\'}")
101       } else {
102         result.append(c)
103       }
104       i++
105     }
106     // If the last-emitted character wasn't a margin '|', add a blank line. This will get removed
107     // by trimMargin().
108     if (!value.endsWith("\n")) result.append("\n")
109     result.append("\"\"\".trimMargin()")
110     return result.toString()
111   } else {
112     val result = StringBuilder(value.length + 32)
113     // using pre-formatted strings allows us to get away with not escaping symbols that would
114     // normally require escaping, e.g. "foo ${"bar"} baz"
115     if (isInsideRawString) result.append("\"\"\"") else result.append('"')
116     for (c in value) {
117       // Trivial case: single quote must not be escaped.
118       if (c == '\'') {
119         result.append("'")
120         continue
121       }
122       // Trivial case: double quotes must be escaped.
123       if (c == '\"' && !isInsideRawString) {
124         result.append("\\\"")
125         continue
126       }
127       // Trivial case: $ signs must be escaped.
128       if (c == '$' && !isInsideRawString) {
129         result.append("\${\'\$\'}")
130         continue
131       }
132       // Default case: just let character literal do its work.
133       result.append(if (isInsideRawString) c else characterLiteralWithoutSingleQuotes(c))
134       // Need to append indent after linefeed?
135     }
136     if (isInsideRawString) result.append("\"\"\"") else result.append('"')
137     return result.toString()
138   }
139 }
140 
ensureEndsWithNewLinenull141 internal fun CodeBlock.ensureEndsWithNewLine() = trimTrailingNewLine('\n')
142 
143 internal fun CodeBlock.trimTrailingNewLine(replaceWith: Char? = null) = if (isEmpty()) {
144   this
145 } else {
<lambda>null146   with(toBuilder()) {
147     val lastFormatPart = trim().formatParts.last()
148     if (lastFormatPart.isPlaceholder && args.isNotEmpty()) {
149       val lastArg = args.last()
150       if (lastArg is String) {
151         val trimmedArg = lastArg.trimEnd('\n')
152         args[args.size - 1] = if (replaceWith != null) {
153           trimmedArg + replaceWith
154         } else {
155           trimmedArg
156         }
157       }
158     } else {
159       formatParts[formatParts.lastIndexOf(lastFormatPart)] = lastFormatPart.trimEnd('\n')
160       if (replaceWith != null) {
161         formatParts += "$replaceWith"
162       }
163     }
164     return@with build()
165   }
166 }
167 
168 private val IDENTIFIER_REGEX =
169   (
170     "((\\p{gc=Lu}+|\\p{gc=Ll}+|\\p{gc=Lt}+|\\p{gc=Lm}+|\\p{gc=Lo}+|\\p{gc=Nl}+)+" +
171       "\\d*" +
172       "\\p{gc=Lu}*\\p{gc=Ll}*\\p{gc=Lt}*\\p{gc=Lm}*\\p{gc=Lo}*\\p{gc=Nl}*)" +
173       "|" +
174       "(`[^\n\r`]+`)"
175     )
176     .toRegex()
177 
178 internal val String.isIdentifier get() = IDENTIFIER_REGEX.matches(this)
179 
180 // https://kotlinlang.org/docs/reference/keyword-reference.html
181 internal val KEYWORDS = setOf(
182   // Hard keywords
183   "as",
184   "break",
185   "class",
186   "continue",
187   "do",
188   "else",
189   "false",
190   "for",
191   "fun",
192   "if",
193   "in",
194   "interface",
195   "is",
196   "null",
197   "object",
198   "package",
199   "return",
200   "super",
201   "this",
202   "throw",
203   "true",
204   "try",
205   "typealias",
206   "typeof",
207   "val",
208   "var",
209   "when",
210   "while",
211 
212   // Soft keywords
213   "by",
214   "catch",
215   "constructor",
216   "delegate",
217   "dynamic",
218   "field",
219   "file",
220   "finally",
221   "get",
222   "import",
223   "init",
224   "param",
225   "property",
226   "receiver",
227   "set",
228   "setparam",
229   "where",
230 
231   // Modifier keywords
232   "actual",
233   "abstract",
234   "annotation",
235   "companion",
236   "const",
237   "crossinline",
238   "data",
239   "enum",
240   "expect",
241   "external",
242   "final",
243   "infix",
244   "inline",
245   "inner",
246   "internal",
247   "lateinit",
248   "noinline",
249   "open",
250   "operator",
251   "out",
252   "override",
253   "private",
254   "protected",
255   "public",
256   "reified",
257   "sealed",
258   "suspend",
259   "tailrec",
260   "value",
261   "vararg",
262 
263   // These aren't keywords anymore but still break some code if unescaped. https://youtrack.jetbrains.com/issue/KT-52315
264   "header",
265   "impl",
266 
267   // Other reserved keywords
268   "yield",
269 )
270 
271 private const val ALLOWED_CHARACTER = '$'
272 
273 private const val UNDERSCORE_CHARACTER = '_'
274 
275 internal val String.isKeyword get() = this in KEYWORDS
276 
<lambda>null277 internal val String.hasAllowedCharacters get() = this.any { it == ALLOWED_CHARACTER }
278 
<lambda>null279 internal val String.allCharactersAreUnderscore get() = this.all { it == UNDERSCORE_CHARACTER }
280 
281 // https://github.com/JetBrains/kotlin/blob/master/compiler/frontend.java/src/org/jetbrains/kotlin/resolve/jvm/checkers/JvmSimpleNameBacktickChecker.kt
282 private val ILLEGAL_CHARACTERS_TO_ESCAPE = setOf('.', ';', '[', ']', '/', '<', '>', ':', '\\')
283 
failIfEscapeInvalidnull284 private fun String.failIfEscapeInvalid() {
285   require(!any { it in ILLEGAL_CHARACTERS_TO_ESCAPE }) {
286     "Can't escape identifier $this because it contains illegal characters: " +
287       ILLEGAL_CHARACTERS_TO_ESCAPE.intersect(this.toSet()).joinToString("")
288   }
289 }
290 
escapeIfNecessarynull291 internal fun String.escapeIfNecessary(validate: Boolean = true): String = escapeIfNotJavaIdentifier()
292   .escapeIfKeyword()
293   .escapeIfHasAllowedCharacters()
294   .escapeIfAllCharactersAreUnderscore()
295   .apply { if (validate) failIfEscapeInvalid() }
296 
297 /**
298  * Because of [KT-18706](https://youtrack.jetbrains.com/issue/KT-18706)
299  * bug all aliases escaped with backticks are not resolved.
300  *
301  * So this method is used instead, which uses custom escape rules:
302  * - if all characters are underscores, add `'0'` to the end
303  * - if it's a keyword, prepend it with double underscore `"__"`
304  * - if first character cannot be used as identifier start (e.g. a number), underscore is prepended
305  * - all `'$'` replaced with double underscore `"__"`
306  * - all characters that cannot be used as identifier part (e.g. space or hyphen) are
307  *   replaced with `"_U<code>"` where `code` is 4-digit Unicode character code in hexadecimal form
308  */
escapeAsAliasnull309 internal fun String.escapeAsAlias(validate: Boolean = true): String {
310   if (allCharactersAreUnderscore) {
311     return "${this}0" // add '0' to make it a valid identifier
312   }
313 
314   if (isKeyword) {
315     return "__$this"
316   }
317 
318   val newAlias = StringBuilder("")
319 
320   if (!Character.isJavaIdentifierStart(first())) {
321     newAlias.append('_')
322   }
323 
324   for (ch in this) {
325     if (ch == ALLOWED_CHARACTER) {
326       newAlias.append("__") // all $ replaced with __
327       continue
328     }
329 
330     if (!Character.isJavaIdentifierPart(ch)) {
331       newAlias.append("_U").append(Integer.toHexString(ch.code).padStart(4, '0'))
332       continue
333     }
334 
335     newAlias.append(ch)
336   }
337 
338   return newAlias.toString().apply { if (validate) failIfEscapeInvalid() }
339 }
340 
alreadyEscapednull341 private fun String.alreadyEscaped() = startsWith("`") && endsWith("`")
342 
343 private fun String.escapeIfKeyword() = if (isKeyword && !alreadyEscaped()) "`$this`" else this
344 
345 private fun String.escapeIfHasAllowedCharacters() = if (hasAllowedCharacters && !alreadyEscaped()) "`$this`" else this
346 
347 private fun String.escapeIfAllCharactersAreUnderscore() = if (allCharactersAreUnderscore && !alreadyEscaped()) "`$this`" else this
348 
349 private fun String.escapeIfNotJavaIdentifier(): String {
350   return if ((
351       !Character.isJavaIdentifierStart(first()) ||
352         drop(1).any { !Character.isJavaIdentifierPart(it) }
353       ) &&
354     !alreadyEscaped()
355   ) {
356     "`$this`".replace(' ', '·')
357   } else {
358     this
359   }
360 }
361 
escapeSegmentsIfNecessarynull362 internal fun String.escapeSegmentsIfNecessary(delimiter: Char = '.') = split(delimiter)
363   .filter { it.isNotEmpty() }
<lambda>null364   .joinToString(delimiter.toString()) { it.escapeIfNecessary() }
365