xref: /aosp_15_r20/external/kotlinpoet/kotlinpoet/src/commonMain/kotlin/com/squareup/kotlinpoet/NameAllocator.kt (revision 3c321d951dd070fb96f8ba59e952ffc3131379a0)
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