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