• 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() = if (isEmpty()) {
142   this
143 } else {
<lambda>null144   with(toBuilder()) {
145     val lastFormatPart = trim().formatParts.last()
146     if (lastFormatPart.isPlaceholder && args.isNotEmpty()) {
147       val lastArg = args.last()
148       if (lastArg is String) {
149         args[args.size - 1] = lastArg.trimEnd('\n') + '\n'
150       }
151     } else {
152       formatParts[formatParts.lastIndexOf(lastFormatPart)] = lastFormatPart.trimEnd('\n')
153       formatParts += "\n"
154     }
155     return@with build()
156   }
157 }
158 
159 private val IDENTIFIER_REGEX =
160   (
161     "((\\p{gc=Lu}+|\\p{gc=Ll}+|\\p{gc=Lt}+|\\p{gc=Lm}+|\\p{gc=Lo}+|\\p{gc=Nl}+)+" +
162       "\\d*" +
163       "\\p{gc=Lu}*\\p{gc=Ll}*\\p{gc=Lt}*\\p{gc=Lm}*\\p{gc=Lo}*\\p{gc=Nl}*)" +
164       "|" +
165       "(`[^\n\r`]+`)"
166     )
167     .toRegex()
168 
169 internal val String.isIdentifier get() = IDENTIFIER_REGEX.matches(this)
170 
171 // https://kotlinlang.org/docs/reference/keyword-reference.html
172 private val KEYWORDS = setOf(
173   // Hard keywords
174   "as",
175   "break",
176   "class",
177   "continue",
178   "do",
179   "else",
180   "false",
181   "for",
182   "fun",
183   "if",
184   "in",
185   "interface",
186   "is",
187   "null",
188   "object",
189   "package",
190   "return",
191   "super",
192   "this",
193   "throw",
194   "true",
195   "try",
196   "typealias",
197   "typeof",
198   "val",
199   "var",
200   "when",
201   "while",
202 
203   // Soft keywords
204   "by",
205   "catch",
206   "constructor",
207   "delegate",
208   "dynamic",
209   "field",
210   "file",
211   "finally",
212   "get",
213   "import",
214   "init",
215   "param",
216   "property",
217   "receiver",
218   "set",
219   "setparam",
220   "where",
221 
222   // Modifier keywords
223   "actual",
224   "abstract",
225   "annotation",
226   "companion",
227   "const",
228   "crossinline",
229   "data",
230   "enum",
231   "expect",
232   "external",
233   "final",
234   "infix",
235   "inline",
236   "inner",
237   "internal",
238   "lateinit",
239   "noinline",
240   "open",
241   "operator",
242   "out",
243   "override",
244   "private",
245   "protected",
246   "public",
247   "reified",
248   "sealed",
249   "suspend",
250   "tailrec",
251   "value",
252   "vararg",
253 
254   // These aren't keywords anymore but still break some code if unescaped. https://youtrack.jetbrains.com/issue/KT-52315
255   "header",
256   "impl",
257 
258   // Other reserved keywords
259   "yield",
260 )
261 
262 private const val ALLOWED_CHARACTER = '$'
263 
264 private const val UNDERSCORE_CHARACTER = '_'
265 
266 internal val String.isKeyword get() = this in KEYWORDS
267 
<lambda>null268 internal val String.hasAllowedCharacters get() = this.any { it == ALLOWED_CHARACTER }
269 
<lambda>null270 internal val String.allCharactersAreUnderscore get() = this.all { it == UNDERSCORE_CHARACTER }
271 
272 // https://github.com/JetBrains/kotlin/blob/master/compiler/frontend.java/src/org/jetbrains/kotlin/resolve/jvm/checkers/JvmSimpleNameBacktickChecker.kt
273 private val ILLEGAL_CHARACTERS_TO_ESCAPE = setOf('.', ';', '[', ']', '/', '<', '>', ':', '\\')
274 
failIfEscapeInvalidnull275 private fun String.failIfEscapeInvalid() {
276   require(!any { it in ILLEGAL_CHARACTERS_TO_ESCAPE }) {
277     "Can't escape identifier $this because it contains illegal characters: " +
278       ILLEGAL_CHARACTERS_TO_ESCAPE.intersect(this.toSet()).joinToString("")
279   }
280 }
281 
escapeIfNecessarynull282 internal fun String.escapeIfNecessary(validate: Boolean = true): String = escapeIfNotJavaIdentifier()
283   .escapeIfKeyword()
284   .escapeIfHasAllowedCharacters()
285   .escapeIfAllCharactersAreUnderscore()
286   .apply { if (validate) failIfEscapeInvalid() }
287 
alreadyEscapednull288 private fun String.alreadyEscaped() = startsWith("`") && endsWith("`")
289 
290 private fun String.escapeIfKeyword() = if (isKeyword && !alreadyEscaped()) "`$this`" else this
291 
292 private fun String.escapeIfHasAllowedCharacters() = if (hasAllowedCharacters && !alreadyEscaped()) "`$this`" else this
293 
294 private fun String.escapeIfAllCharactersAreUnderscore() = if (allCharactersAreUnderscore && !alreadyEscaped()) "`$this`" else this
295 
296 private fun String.escapeIfNotJavaIdentifier(): String {
297   return if ((
298       !Character.isJavaIdentifierStart(first()) ||
299         drop(1).any { !Character.isJavaIdentifierPart(it) }
300       ) &&
301     !alreadyEscaped()
302   ) {
303     "`$this`".replace(' ', '·')
304   } else {
305     this
306   }
307 }
308 
escapeSegmentsIfNecessarynull309 internal fun String.escapeSegmentsIfNecessary(delimiter: Char = '.') = split(delimiter)
310   .filter { it.isNotEmpty() }
<lambda>null311   .joinToString(delimiter.toString()) { it.escapeIfNecessary() }
312