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