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 java.util.UUID
19
20 /**
21 * Assigns Kotlin identifier names to avoid collisions, keywords, and invalid characters. To use,
22 * first create an instance and allocate all of the names that you need. Typically this is a
23 * mix of user-supplied names and constants:
24 *
25 * ```kotlin
26 * val nameAllocator = NameAllocator()
27 * for (property in properties) {
28 * nameAllocator.newName(property.name, property)
29 * }
30 * nameAllocator.newName("sb", "string builder")
31 * ```
32 *
33 * Pass a unique tag object to each allocation. The tag scopes the name, and can be used to look up
34 * the allocated name later. Typically the tag is the object that is being named. In the above
35 * example we use `property` for the user-supplied property names, and `"string builder"` for our
36 * constant string builder.
37 *
38 * Once we've allocated names we can use them when generating code:
39 *
40 * ```kotlin
41 * val builder = FunSpec.builder("toString")
42 * .addModifiers(KModifier.OVERRIDE)
43 * .returns(String::class)
44 *
45 * builder.addStatement("val %N = %T()",
46 * nameAllocator.get("string builder"), StringBuilder::class)
47 *
48 * for (property in properties) {
49 * builder.addStatement("%N.append(%N)",
50 * nameAllocator.get("string builder"), nameAllocator.get(property))
51 * }
52 * builder.addStatement("return %N.toString()", nameAllocator.get("string builder"))
53 * return builder.build()
54 * ```
55 *
56 * The above code generates unique names if presented with conflicts. Given user-supplied properties
57 * with names `ab` and `sb` this generates the following:
58 *
59 * ```kotlin
60 * override fun toString(): kotlin.String {
61 * val sb_ = java.lang.StringBuilder()
62 * sb_.append(ab)
63 * sb_.append(sb)
64 * return sb_.toString()
65 * }
66 * ```
67 *
68 * The underscore is appended to `sb` to avoid conflicting with the user-supplied `sb` property.
69 * Underscores are also prefixed for names that start with a digit, and used to replace name-unsafe
70 * characters like space or dash.
71 *
72 * When dealing with multiple independent inner scopes, use a [copy][NameAllocator.copy] of the
73 * NameAllocator used for the outer scope to further refine name allocation for a specific inner
74 * scope.
75 */
76 public class NameAllocator private constructor(
77 private val allocatedNames: MutableSet<String>,
78 private val tagToName: MutableMap<Any, String>,
79 ) {
80 public constructor() : this(preallocateKeywords = true)
81
82 /**
83 * @param preallocateKeywords If true, all Kotlin keywords will be preallocated. Requested names which
84 * collide with keywords will be suffixed with underscores to avoid being used as identifiers:
85 *
86 * ```kotlin
87 * val nameAllocator = NameAllocator(preallocateKeywords = true)
88 * println(nameAllocator.newName("when")) // prints "when_"
89 * ```
90 *
91 * If false, keywords will not get any special treatment:
92 *
93 * ```kotlin
94 * val nameAllocator = NameAllocator(preallocateKeywords = false)
95 * println(nameAllocator.newName("when")) // prints "when"
96 * ```
97 *
98 * Note that you can use the `%N` placeholder when emitting a name produced by [NameAllocator] to
99 * ensure it's properly escaped for use as an identifier:
100 *
101 * ```kotlin
102 * val nameAllocator = NameAllocator(preallocateKeywords = false)
103 * println(CodeBlock.of("%N", nameAllocator.newName("when"))) // prints "`when`"
104 * ```
105 *
106 * The default behaviour of [NameAllocator] is to preallocate keywords - this is the behaviour you'll
107 * get when using the no-arg constructor.
108 */
109 public constructor(preallocateKeywords: Boolean) : this(
110 allocatedNames = if (preallocateKeywords) KEYWORDS.toMutableSet() else mutableSetOf(),
111 tagToName = mutableMapOf(),
112 )
113
114 /**
115 * Return a new name using `suggestion` that will not be a Java identifier or clash with other
116 * names. The returned value can be queried multiple times by passing `tag` to
117 * [NameAllocator.get].
118 */
newNamenull119 @JvmOverloads public fun newName(
120 suggestion: String,
121 tag: Any = UUID.randomUUID().toString(),
122 ): String {
123 var result = toJavaIdentifier(suggestion)
124 while (!allocatedNames.add(result)) {
125 result += "_"
126 }
127
128 val replaced = tagToName.put(tag, result)
129 if (replaced != null) {
130 tagToName[tag] = replaced // Put things back as they were!
131 throw IllegalArgumentException("tag $tag cannot be used for both '$replaced' and '$result'")
132 }
133
134 return result
135 }
136
137 /** Retrieve a name created with [NameAllocator.newName]. */
<lambda>null138 public operator fun get(tag: Any): String = requireNotNull(tagToName[tag]) { "unknown tag: $tag" }
139
140 /**
141 * Create a deep copy of this NameAllocator. Useful to create multiple independent refinements
142 * of a NameAllocator to be used in the respective definition of multiples, independently-scoped,
143 * inner code blocks.
144 *
145 * @return A deep copy of this NameAllocator.
146 */
copynull147 public fun copy(): NameAllocator {
148 return NameAllocator(allocatedNames.toMutableSet(), tagToName.toMutableMap())
149 }
150 }
151
<lambda>null152 private fun toJavaIdentifier(suggestion: String) = buildString {
153 var i = 0
154 while (i < suggestion.length) {
155 val codePoint = suggestion.codePointAt(i)
156 if (i == 0 &&
157 !Character.isJavaIdentifierStart(codePoint) &&
158 Character.isJavaIdentifierPart(codePoint)
159 ) {
160 append("_")
161 }
162
163 val validCodePoint: Int = if (Character.isJavaIdentifierPart(codePoint)) {
164 codePoint
165 } else {
166 '_'.code
167 }
168 appendCodePoint(validCodePoint)
169 i += Character.charCount(codePoint)
170 }
171 }
172