1 /*
2 * Copyright (C) 2014 Google, 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 @file:JvmName("ClassNames")
17
18 package com.squareup.kotlinpoet
19
20 import javax.lang.model.element.Element
21 import javax.lang.model.element.ElementKind
22 import javax.lang.model.element.NestingKind.MEMBER
23 import javax.lang.model.element.NestingKind.TOP_LEVEL
24 import javax.lang.model.element.PackageElement
25 import javax.lang.model.element.TypeElement
26 import kotlin.reflect.KClass
27
28 /** A fully-qualified class name for top-level and member classes. */
29 public class ClassName internal constructor(
30 names: List<String>,
31 nullable: Boolean = false,
32 annotations: List<AnnotationSpec> = emptyList(),
33 tags: Map<KClass<*>, Any> = emptyMap()
34 ) : TypeName(nullable, annotations, TagMap(tags)), Comparable<ClassName> {
35 /**
36 * Returns a class name created from the given parts. For example, calling this with package name
37 * `"java.util"` and simple names `"Map"`, `"Entry"` yields `Map.Entry`.
38 */
39 @Deprecated("", level = DeprecationLevel.HIDDEN)
40 public constructor(packageName: String, simpleName: String, vararg simpleNames: String) :
41 this(listOf(packageName, simpleName, *simpleNames))
42
43 @Deprecated(
44 "Simple names must not be empty. Did you forget an argument?",
45 level = DeprecationLevel.ERROR,
46 replaceWith = ReplaceWith("ClassName(packageName, TODO())"),
47 )
48 public constructor(packageName: String) : this(packageName, listOf())
49
50 /**
51 * Returns a class name created from the given parts. For example, calling this with package name
52 * `"java.util"` and simple names `"Map"`, `"Entry"` yields `Map.Entry`.
53 */
54 public constructor(packageName: String, vararg simpleNames: String) :
55 this(listOf(packageName, *simpleNames)) {
<lambda>null56 require(simpleNames.isNotEmpty()) { "simpleNames must not be empty" }
<lambda>null57 require(simpleNames.none { it.isEmpty() }) {
58 "simpleNames must not contain empty items: ${simpleNames.contentToString()}"
59 }
60 }
61
62 /**
63 * Returns a class name created from the given parts. For example, calling this with package name
64 * `"java.util"` and simple names `"Map"`, `"Entry"` yields `Map.Entry`.
65 */
66 public constructor(packageName: String, simpleNames: List<String>) :
<lambda>null67 this(mutableListOf(packageName).apply { addAll(simpleNames) }) {
<lambda>null68 require(simpleNames.isNotEmpty()) { "simpleNames must not be empty" }
<lambda>null69 require(simpleNames.none { it.isEmpty() }) {
70 "simpleNames must not contain empty items: $simpleNames"
71 }
72 }
73
74 /** From top to bottom. This will be `["java.util", "Map", "Entry"]` for `Map.Entry`. */
75 private val names = names.toImmutableList()
76
77 /** Fully qualified name using `.` as a separator, like `kotlin.collections.Map.Entry`. */
78 public val canonicalName: String = if (names[0].isEmpty())
79 names.subList(1, names.size).joinToString(".") else
80 names.joinToString(".")
81
82 /** Package name, like `"kotlin.collections"` for `Map.Entry`. */
83 public val packageName: String get() = names[0]
84
85 /** Simple name of this class, like `"Entry"` for `Map.Entry`. */
86 public val simpleName: String get() = names[names.size - 1]
87
88 /**
89 * The enclosing classes, outermost first, followed by the simple name. This is `["Map", "Entry"]`
90 * for `Map.Entry`.
91 */
92 public val simpleNames: List<String> get() = names.subList(1, names.size)
93
copynull94 override fun copy(
95 nullable: Boolean,
96 annotations: List<AnnotationSpec>,
97 tags: Map<KClass<*>, Any>
98 ): ClassName {
99 return ClassName(names, nullable, annotations, tags)
100 }
101
102 /**
103 * Returns the enclosing class, like `Map` for `Map.Entry`. Returns null if this class is not
104 * nested in another class.
105 */
enclosingClassNamenull106 public fun enclosingClassName(): ClassName? {
107 return if (names.size != 2)
108 ClassName(names.subList(0, names.size - 1)) else
109 null
110 }
111
112 /**
113 * Returns the top class in this nesting group. Equivalent to chained calls to
114 * [ClassName.enclosingClassName] until the result's enclosing class is null.
115 */
topLevelClassNamenull116 public fun topLevelClassName(): ClassName = ClassName(names.subList(0, 2))
117
118 /**
119 * Fully qualified name using `.` to separate package from the top level class name, and `$` to
120 * separate nested classes, like `kotlin.collections.Map$Entry`.
121 */
122 public fun reflectionName(): String {
123 // trivial case: no nested names
124 if (names.size == 2) {
125 return if (packageName.isEmpty())
126 names[1] else
127 packageName + "." + names[1]
128 }
129 // concat top level class name and nested names
130 return buildString {
131 append(topLevelClassName().canonicalName)
132 for (name in simpleNames.subList(1, simpleNames.size)) {
133 append('$').append(name)
134 }
135 }
136 }
137
138 /**
139 * Callable reference to the constructor of this class. Emits the enclosing class if one exists,
140 * followed by the reference operator `::`, followed by either [simpleName] or the
141 * fully-qualified name if this is a top-level class.
142 *
143 * Note: As `::$packageName.$simpleName` is not valid syntax, an aliased import may be required
144 * for a top-level class with a conflicting name.
145 */
constructorReferencenull146 public fun constructorReference(): CodeBlock {
147 val enclosing = enclosingClassName()
148 return if (enclosing != null) {
149 CodeBlock.of("%T::%N", enclosing, simpleName)
150 } else {
151 CodeBlock.of("::%T", this)
152 }
153 }
154
155 /** Returns a new [ClassName] instance for the specified `name` as nested inside this class. */
nestedClassnull156 public fun nestedClass(name: String): ClassName = ClassName(names + name)
157
158 /**
159 * Returns a class that shares the same enclosing package or class. If this class is enclosed by
160 * another class, this is equivalent to `enclosingClassName().nestedClass(name)`. Otherwise
161 * it is equivalent to `get(packageName(), name)`.
162 */
163 public fun peerClass(name: String): ClassName {
164 val result = names.toMutableList()
165 result[result.size - 1] = name
166 return ClassName(result)
167 }
168
169 /**
170 * Orders by the fully-qualified name. Nested types are ordered immediately after their
171 * enclosing type. For example, the following types are ordered by this method:
172 *
173 * ```
174 * com.example.Robot
175 * com.example.Robot.Motor
176 * com.example.RoboticVacuum
177 * ```
178 */
compareTonull179 override fun compareTo(other: ClassName): Int = canonicalName.compareTo(other.canonicalName)
180
181 override fun emit(out: CodeWriter) =
182 out.emit(out.lookupName(this).escapeSegmentsIfNecessary())
183
184 public companion object {
185 /**
186 * Returns a new [ClassName] instance for the given fully-qualified class name string. This
187 * method assumes that the input is ASCII and follows typical Java style (lowercase package
188 * names, UpperCamelCase class names) and may produce incorrect results or throw
189 * [IllegalArgumentException] otherwise. For that reason, the constructor should be preferred as
190 * it can create [ClassName] instances without such restrictions.
191 */
192 @JvmStatic public fun bestGuess(classNameString: String): ClassName {
193 val names = mutableListOf<String>()
194
195 // Add the package name, like "java.util.concurrent", or "" for no package.
196 var p = 0
197 while (p < classNameString.length && Character.isLowerCase(classNameString.codePointAt(p))) {
198 p = classNameString.indexOf('.', p) + 1
199 require(p != 0) { "couldn't make a guess for $classNameString" }
200 }
201 names += if (p != 0) classNameString.substring(0, p - 1) else ""
202
203 // Add the class names, like "Map" and "Entry".
204 for (part in classNameString.substring(p).split('.')) {
205 require(part.isNotEmpty() && Character.isUpperCase(part.codePointAt(0))) {
206 "couldn't make a guess for $classNameString"
207 }
208
209 names += part
210 }
211
212 require(names.size >= 2) { "couldn't make a guess for $classNameString" }
213 return ClassName(names)
214 }
215 }
216 }
217
218 @DelicateKotlinPoetApi(
219 message = "Java reflection APIs don't give complete information on Kotlin types. Consider using" +
220 " the kotlinpoet-metadata APIs instead."
221 )
222 @JvmName("get")
asClassNamenull223 public fun Class<*>.asClassName(): ClassName {
224 require(!isPrimitive) { "primitive types cannot be represented as a ClassName" }
225 require(Void.TYPE != this) { "'void' type cannot be represented as a ClassName" }
226 require(!isArray) { "array types cannot be represented as a ClassName" }
227 val names = mutableListOf<String>()
228 var c = this
229 while (true) {
230 names += c.simpleName
231 val enclosing = c.enclosingClass ?: break
232 c = enclosing
233 }
234 // Avoid unreliable Class.getPackage(). https://github.com/square/javapoet/issues/295
235 val lastDot = c.name.lastIndexOf('.')
236 if (lastDot != -1) names += c.name.substring(0, lastDot)
237 names.reverse()
238 return ClassName(names)
239 }
240
241 @JvmName("get")
asClassNamenull242 public fun KClass<*>.asClassName(): ClassName {
243 qualifiedName?.let { return ClassName.bestGuess(it) }
244 throw IllegalArgumentException("$this cannot be represented as a ClassName")
245 }
246
247 /** Returns the class name for `element`. */
248 @DelicateKotlinPoetApi(
249 message = "Element APIs don't give complete information on Kotlin types. Consider using" +
250 " the kotlinpoet-metadata APIs instead."
251 )
252 @JvmName("get")
asClassNamenull253 public fun TypeElement.asClassName(): ClassName {
254 fun isClassOrInterface(e: Element) = e.kind.isClass || e.kind.isInterface
255
256 fun getPackage(type: Element): PackageElement {
257 var t = type
258 while (t.kind != ElementKind.PACKAGE) {
259 t = t.enclosingElement
260 }
261 return t as PackageElement
262 }
263
264 val names = mutableListOf<String>()
265 var e: Element = this
266 while (isClassOrInterface(e)) {
267 val eType = e as TypeElement
268 require(eType.nestingKind.isOneOf(TOP_LEVEL, MEMBER)) {
269 "unexpected type testing"
270 }
271 names += eType.simpleName.toString()
272 e = eType.enclosingElement
273 }
274 names += getPackage(this).qualifiedName.toString()
275 names.reverse()
276 return ClassName(names)
277 }
278