xref: /aosp_15_r20/external/kotlinpoet/kotlinpoet/src/commonMain/kotlin/com/squareup/kotlinpoet/FileSpec.kt (revision 3c321d951dd070fb96f8ba59e952ffc3131379a0)
1 /*
<lambda>null2  * 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.AnnotationSpec.UseSiteTarget.FILE
19 import java.io.ByteArrayInputStream
20 import java.io.File
21 import java.io.IOException
22 import java.io.InputStream
23 import java.net.URI
24 import java.nio.charset.StandardCharsets.UTF_8
25 import java.nio.file.Path
26 import javax.annotation.processing.Filer
27 import javax.tools.JavaFileObject
28 import javax.tools.JavaFileObject.Kind
29 import javax.tools.SimpleJavaFileObject
30 import javax.tools.StandardLocation
31 import kotlin.DeprecationLevel.HIDDEN
32 import kotlin.io.path.createDirectories
33 import kotlin.io.path.isDirectory
34 import kotlin.io.path.notExists
35 import kotlin.io.path.outputStream
36 import kotlin.reflect.KClass
37 
38 /**
39  * A Kotlin file containing top level objects like classes, objects, functions, properties, and type
40  * aliases.
41  *
42  * Items are output in the following order:
43  * - Comment
44  * - Annotations
45  * - Package
46  * - Imports
47  * - Members
48  */
49 public class FileSpec private constructor(
50   builder: Builder,
51   private val tagMap: TagMap = builder.buildTagMap(),
52 ) : Taggable by tagMap, Annotatable, TypeSpecHolder, MemberSpecHolder {
53   override val annotations: List<AnnotationSpec> = builder.annotations.toImmutableList()
54   override val typeSpecs: List<TypeSpec> = builder.members.filterIsInstance<TypeSpec>().toImmutableList()
55   override val propertySpecs: List<PropertySpec> = builder.members.filterIsInstance<PropertySpec>().toImmutableList()
56   override val funSpecs: List<FunSpec> = builder.members.filterIsInstance<FunSpec>().toImmutableList()
57   public val comment: CodeBlock = builder.comment.build()
58   public val packageName: String = builder.packageName
59   public val name: String = builder.name
60   public val members: List<Any> = builder.members.toList()
61   public val defaultImports: Set<String> = builder.defaultImports.toSet()
62   public val body: CodeBlock = builder.body.build()
63   public val isScript: Boolean = builder.isScript
64   private val memberImports = builder.memberImports.associateBy(Import::qualifiedName)
65   private val indent = builder.indent
66   private val extension = if (isScript) "kts" else "kt"
67 
68   /**
69    * The relative path of the file which would be produced by a call to [writeTo].
70    * This value always uses unix-style path separators (`/`).
71    */
72   public val relativePath: String = buildString {
73     for (packageComponent in packageName.split('.').dropLastWhile { it.isEmpty() }) {
74       append(packageComponent)
75       append('/')
76     }
77     append(name)
78     append('.')
79     append(extension)
80   }
81 
82   @Throws(IOException::class)
83   public fun writeTo(out: Appendable) {
84     val codeWriter = CodeWriter.withCollectedImports(
85       out = out,
86       indent = indent,
87       memberImports = memberImports,
88       emitStep = { importsCollector -> emit(importsCollector, collectingImports = true) },
89     )
90     emit(codeWriter, collectingImports = false)
91     codeWriter.close()
92   }
93 
94   @Deprecated("", level = HIDDEN)
95   @JvmName("writeTo") // For binary compatibility.
96   public fun oldWriteTo(directory: Path) {
97     writeTo(directory)
98   }
99 
100   /**
101    * Writes this to [directory] as UTF-8 using the standard directory structure
102    * and returns the newly output path.
103    */
104   @Throws(IOException::class)
105   public fun writeTo(directory: Path): Path {
106     require(directory.notExists() || directory.isDirectory()) {
107       "path $directory exists but is not a directory."
108     }
109     val outputPath = directory.resolve(relativePath)
110     outputPath.parent.createDirectories()
111     outputPath.outputStream().bufferedWriter().use(::writeTo)
112     return outputPath
113   }
114 
115   @Deprecated("", level = HIDDEN)
116   @JvmName("writeTo") // For binary compatibility.
117   public fun oldWriteTo(directory: File) {
118     writeTo(directory)
119   }
120 
121   /**
122    * Writes this to [directory] as UTF-8 using the standard directory structure
123    * and returns the newly output file.
124    */
125   @Throws(IOException::class)
126   public fun writeTo(directory: File): File = writeTo(directory.toPath()).toFile()
127 
128   /** Writes this to `filer`.  */
129   @Throws(IOException::class)
130   public fun writeTo(filer: Filer) {
131     val originatingElements = members.asSequence()
132       .filterIsInstance<OriginatingElementsHolder>()
133       .flatMap { it.originatingElements.asSequence() }
134       .toSet()
135     val filerSourceFile = filer.createResource(
136       StandardLocation.SOURCE_OUTPUT,
137       packageName,
138       "$name.$extension",
139       *originatingElements.toTypedArray(),
140     )
141     try {
142       filerSourceFile.openWriter().use { writer -> writeTo(writer) }
143     } catch (e: Exception) {
144       try {
145         filerSourceFile.delete()
146       } catch (_: Exception) {
147       }
148       throw e
149     }
150   }
151 
152   private fun emit(codeWriter: CodeWriter, collectingImports: Boolean) {
153     if (comment.isNotEmpty()) {
154       codeWriter.emitComment(comment)
155     }
156 
157     if (annotations.isNotEmpty()) {
158       codeWriter.emitAnnotations(annotations, inline = false)
159       codeWriter.emit("\n")
160     }
161 
162     codeWriter.pushPackage(packageName)
163 
164     val escapedPackageName = packageName.escapeSegmentsIfNecessary()
165 
166     if (escapedPackageName.isNotEmpty()) {
167       codeWriter.emitCode("package·%L\n", escapedPackageName)
168       codeWriter.emit("\n")
169     }
170 
171     // If we don't have default imports or are collecting them, we don't need to filter
172     var isDefaultImport: (String) -> Boolean = { false }
173     if (!collectingImports && defaultImports.isNotEmpty()) {
174       val defaultImports = defaultImports.map(String::escapeSegmentsIfNecessary)
175       isDefaultImport = { importName ->
176         importName.substringBeforeLast(".") in defaultImports
177       }
178     }
179     // Aliased imports should always appear at the bottom of the imports list.
180     val (aliasedImports, nonAliasedImports) = codeWriter.imports.values
181       .partition { it.alias != null }
182     val imports = nonAliasedImports.asSequence().map { it.toString() }
183       .filterNot(isDefaultImport)
184       .toSortedSet()
185       .plus(aliasedImports.map { it.toString() }.toSortedSet())
186 
187     if (imports.isNotEmpty()) {
188       for (import in imports) {
189         codeWriter.emitCode("import·%L", import)
190         codeWriter.emit("\n")
191       }
192       codeWriter.emit("\n")
193     }
194 
195     if (isScript) {
196       codeWriter.emitCode(body, omitImplicitModifiers = true)
197     } else {
198       members.forEachIndexed { index, member ->
199         if (index > 0) codeWriter.emit("\n")
200         when (member) {
201           is TypeSpec -> member.emit(codeWriter, null)
202           is FunSpec -> member.emit(codeWriter, null, setOf(KModifier.PUBLIC), true)
203           is PropertySpec -> member.emit(codeWriter, setOf(KModifier.PUBLIC))
204           is TypeAliasSpec -> member.emit(codeWriter)
205           else -> throw AssertionError()
206         }
207       }
208     }
209 
210     codeWriter.popPackage()
211   }
212 
213   override fun equals(other: Any?): Boolean {
214     if (this === other) return true
215     if (other == null) return false
216     if (javaClass != other.javaClass) return false
217     return toString() == other.toString()
218   }
219 
220   override fun hashCode(): Int = toString().hashCode()
221 
222   override fun toString(): String = buildString { writeTo(this) }
223 
224   public fun toJavaFileObject(): JavaFileObject {
225     val uri = URI.create(relativePath)
226     return object : SimpleJavaFileObject(uri, Kind.SOURCE) {
227       private val lastModified = System.currentTimeMillis()
228       override fun getCharContent(ignoreEncodingErrors: Boolean): String {
229         return this@FileSpec.toString()
230       }
231 
232       override fun openInputStream(): InputStream {
233         return ByteArrayInputStream(getCharContent(true).toByteArray(UTF_8))
234       }
235 
236       override fun getLastModified() = lastModified
237     }
238   }
239 
240   @JvmOverloads
241   public fun toBuilder(packageName: String = this.packageName, name: String = this.name): Builder {
242     val builder = Builder(packageName, name, isScript)
243     builder.annotations.addAll(annotations)
244     builder.comment.add(comment)
245     builder.members.addAll(this.members)
246     builder.indent = indent
247     builder.memberImports.addAll(memberImports.values)
248     builder.defaultImports.addAll(defaultImports)
249     builder.tags += tagMap.tags
250     builder.body.add(body)
251     return builder
252   }
253 
254   public class Builder internal constructor(
255     public val packageName: String,
256     public val name: String,
257     public val isScript: Boolean,
258   ) : Taggable.Builder<Builder>,
259     Annotatable.Builder<Builder>,
260     TypeSpecHolder.Builder<Builder>,
261     MemberSpecHolder.Builder<Builder> {
262 
263     override val annotations: MutableList<AnnotationSpec> = mutableListOf()
264     internal val comment = CodeBlock.builder()
265     internal val memberImports = sortedSetOf<Import>()
266     internal var indent = DEFAULT_INDENT
267     override val tags: MutableMap<KClass<*>, Any> = mutableMapOf()
268 
269     public val defaultImports: MutableSet<String> = mutableSetOf()
270     public val imports: List<Import> get() = memberImports.toList()
271     public val members: MutableList<Any> = mutableListOf()
272     internal val body = CodeBlock.builder()
273 
274     /**
275      * Add an annotation to the file.
276      *
277      * The annotation must either have a [`file` use-site target][AnnotationSpec.UseSiteTarget.FILE]
278      * or not have a use-site target specified (in which case it will be changed to `file`).
279      */
280     override fun addAnnotation(annotationSpec: AnnotationSpec): Builder = apply {
281       val spec = when (annotationSpec.useSiteTarget) {
282         FILE -> annotationSpec
283         null -> annotationSpec.toBuilder().useSiteTarget(FILE).build()
284         else -> error(
285           "Use-site target ${annotationSpec.useSiteTarget} not supported for file annotations.",
286         )
287       }
288       annotations += spec
289     }
290 
291     /** Adds a file-site comment. This is prefixed to the start of the file and different from [addBodyComment]. */
292     public fun addFileComment(format: String, vararg args: Any): Builder = apply {
293       comment.add(format.replace(' ', '·'), *args)
294     }
295 
296     @Deprecated(
297       "Use addFileComment() instead.",
298       ReplaceWith("addFileComment(format, args)"),
299       DeprecationLevel.ERROR,
300     )
301     public fun addComment(format: String, vararg args: Any): Builder = addFileComment(format, *args)
302 
303     public fun clearComment(): Builder = apply {
304       comment.clear()
305     }
306 
307     override fun addType(typeSpec: TypeSpec): Builder = apply {
308       if (isScript) {
309         body.add("%L", typeSpec)
310       } else {
311         members += typeSpec
312       }
313     }
314 
315     //region Overrides for binary compatibility
316     @Suppress("RedundantOverride")
317     override fun addTypes(typeSpecs: Iterable<TypeSpec>): Builder = super.addTypes(typeSpecs)
318     //endregion
319 
320     override fun addFunction(funSpec: FunSpec): Builder = apply {
321       require(!funSpec.isConstructor && !funSpec.isAccessor) {
322         "cannot add ${funSpec.name} to file $name"
323       }
324       if (isScript) {
325         body.add("%L", funSpec)
326       } else {
327         members += funSpec
328       }
329     }
330 
331     override fun addProperty(propertySpec: PropertySpec): Builder = apply {
332       if (isScript) {
333         body.add("%L", propertySpec)
334       } else {
335         members += propertySpec
336       }
337     }
338 
339     public fun addTypeAlias(typeAliasSpec: TypeAliasSpec): Builder = apply {
340       if (isScript) {
341         body.add("%L", typeAliasSpec)
342       } else {
343         members += typeAliasSpec
344       }
345     }
346 
347     public fun addImport(constant: Enum<*>): Builder = addImport(
348       constant.declaringJavaClass.asClassName(),
349       constant.name,
350     )
351 
352     public fun addImport(`class`: Class<*>, vararg names: String): Builder = apply {
353       require(names.isNotEmpty()) { "names array is empty" }
354       addImport(`class`.asClassName(), names.toList())
355     }
356 
357     public fun addImport(`class`: KClass<*>, vararg names: String): Builder = apply {
358       require(names.isNotEmpty()) { "names array is empty" }
359       addImport(`class`.asClassName(), names.toList())
360     }
361 
362     public fun addImport(className: ClassName, vararg names: String): Builder = apply {
363       require(names.isNotEmpty()) { "names array is empty" }
364       addImport(className, names.toList())
365     }
366 
367     public fun addImport(`class`: Class<*>, names: Iterable<String>): Builder =
368       addImport(`class`.asClassName(), names)
369 
370     public fun addImport(`class`: KClass<*>, names: Iterable<String>): Builder =
371       addImport(`class`.asClassName(), names)
372 
373     public fun addImport(className: ClassName, names: Iterable<String>): Builder = apply {
374       require("*" !in names) { "Wildcard imports are not allowed" }
375       for (name in names) {
376         memberImports += Import(className.canonicalName + "." + name)
377       }
378     }
379 
380     public fun addImport(packageName: String, vararg names: String): Builder = apply {
381       require(names.isNotEmpty()) { "names array is empty" }
382       addImport(packageName, names.toList())
383     }
384 
385     public fun addImport(packageName: String, names: Iterable<String>): Builder = apply {
386       require("*" !in names) { "Wildcard imports are not allowed" }
387       for (name in names) {
388         memberImports += if (packageName.isNotEmpty()) {
389           Import("$packageName.$name")
390         } else {
391           Import(name)
392         }
393       }
394     }
395 
396     public fun addImport(import: Import): Builder = apply {
397       memberImports += import
398     }
399 
400     public fun clearImports(): Builder = apply {
401       memberImports.clear()
402     }
403 
404     public fun addAliasedImport(`class`: Class<*>, `as`: String): Builder =
405       addAliasedImport(`class`.asClassName(), `as`)
406 
407     public fun addAliasedImport(`class`: KClass<*>, `as`: String): Builder =
408       addAliasedImport(`class`.asClassName(), `as`)
409 
410     public fun addAliasedImport(className: ClassName, `as`: String): Builder = apply {
411       memberImports += Import(className.canonicalName, `as`)
412     }
413 
414     public fun addAliasedImport(
415       className: ClassName,
416       memberName: String,
417       `as`: String,
418     ): Builder = apply {
419       memberImports += Import("${className.canonicalName}.$memberName", `as`)
420     }
421 
422     public fun addAliasedImport(memberName: MemberName, `as`: String): Builder = apply {
423       memberImports += Import(memberName.canonicalName, `as`)
424     }
425 
426     /**
427      * Adds a default import for the given [packageName].
428      *
429      * The format of this should be the qualified name of the package, e.g. `kotlin`, `java.lang`,
430      * `org.gradle.api`, etc.
431      */
432     public fun addDefaultPackageImport(packageName: String): Builder = apply {
433       defaultImports += packageName
434     }
435 
436     /**
437      * Adds Kotlin's standard default package imports as described
438      * [here](https://kotlinlang.org/docs/packages.html#default-imports).
439      */
440     public fun addKotlinDefaultImports(
441       includeJvm: Boolean = true,
442       includeJs: Boolean = true,
443     ): Builder = apply {
444       defaultImports += KOTLIN_DEFAULT_IMPORTS
445       if (includeJvm) {
446         defaultImports += KOTLIN_DEFAULT_JVM_IMPORTS
447       }
448       if (includeJs) {
449         defaultImports += KOTLIN_DEFAULT_JS_IMPORTS
450       }
451     }
452 
453     public fun indent(indent: String): Builder = apply {
454       this.indent = indent
455     }
456 
457     public fun addCode(format: String, vararg args: Any?): Builder = apply {
458       check(isScript) {
459         "addCode() is only allowed in script files"
460       }
461       body.add(format, *args)
462     }
463 
464     public fun addNamedCode(format: String, args: Map<String, *>): Builder = apply {
465       check(isScript) {
466         "addNamedCode() is only allowed in script files"
467       }
468       body.addNamed(format, args)
469     }
470 
471     public fun addCode(codeBlock: CodeBlock): Builder = apply {
472       check(isScript) {
473         "addCode() is only allowed in script files"
474       }
475       body.add(codeBlock)
476     }
477 
478     /** Adds a comment to the body of this script file in the order that it was added. */
479     public fun addBodyComment(format: String, vararg args: Any): Builder = apply {
480       check(isScript) {
481         "addBodyComment() is only allowed in script files"
482       }
483       body.add("//·${format.replace(' ', '·')}\n", *args)
484     }
485 
486     /**
487      * @param controlFlow the control flow construct and its code, such as "if (foo == 5)".
488      * Shouldn't contain braces or newline characters.
489      */
490     public fun beginControlFlow(controlFlow: String, vararg args: Any): Builder = apply {
491       check(isScript) {
492         "beginControlFlow() is only allowed in script files"
493       }
494       body.beginControlFlow(controlFlow, *args)
495     }
496 
497     /**
498      * @param controlFlow the control flow construct and its code, such as "else if (foo == 10)".
499      * Shouldn't contain braces or newline characters.
500      */
501     public fun nextControlFlow(controlFlow: String, vararg args: Any): Builder = apply {
502       check(isScript) {
503         "nextControlFlow() is only allowed in script files"
504       }
505       body.nextControlFlow(controlFlow, *args)
506     }
507 
508     public fun endControlFlow(): Builder = apply {
509       check(isScript) {
510         "endControlFlow() is only allowed in script files"
511       }
512       body.endControlFlow()
513     }
514 
515     public fun addStatement(format: String, vararg args: Any): Builder = apply {
516       check(isScript) {
517         "addStatement() is only allowed in script files"
518       }
519       body.addStatement(format, *args)
520     }
521 
522     public fun clearBody(): Builder = apply {
523       check(isScript) {
524         "clearBody() is only allowed in script files"
525       }
526       body.clear()
527     }
528 
529     //region Overrides for binary compatibility
530     @Suppress("RedundantOverride")
531     override fun addAnnotations(annotationSpecs: Iterable<AnnotationSpec>): Builder =
532       super.addAnnotations(annotationSpecs)
533 
534     @Suppress("RedundantOverride")
535     override fun addAnnotation(annotation: ClassName): Builder = super.addAnnotation(annotation)
536 
537     @DelicateKotlinPoetApi(
538       message = "Java reflection APIs don't give complete information on Kotlin types. Consider " +
539         "using the kotlinpoet-metadata APIs instead.",
540     )
541     override fun addAnnotation(annotation: Class<*>): Builder = super.addAnnotation(annotation)
542 
543     @Suppress("RedundantOverride")
544     override fun addAnnotation(annotation: KClass<*>): Builder = super.addAnnotation(annotation)
545     //endregion
546 
547     public fun build(): FileSpec {
548       for (annotationSpec in annotations) {
549         if (annotationSpec.useSiteTarget != FILE) {
550           error(
551             "Use-site target ${annotationSpec.useSiteTarget} not supported for file annotations.",
552           )
553         }
554       }
555       return FileSpec(this)
556     }
557   }
558 
559   public companion object {
560     @JvmStatic public fun get(packageName: String, typeSpec: TypeSpec): FileSpec {
561       val fileName = typeSpec.name
562         ?: throw IllegalArgumentException("file name required but type has no name")
563       return builder(packageName, fileName).addType(typeSpec).build()
564     }
565 
566     @JvmStatic public fun builder(className: ClassName): Builder {
567       require(className.simpleNames.size == 1) {
568         "nested types can't be used to name a file: ${className.simpleNames.joinToString(".")}"
569       }
570       return builder(className.packageName, className.simpleName)
571     }
572 
573     @JvmStatic public fun builder(memberName: MemberName): Builder {
574       return builder(memberName.packageName, memberName.simpleName)
575     }
576 
577     @JvmStatic public fun builder(packageName: String, fileName: String): Builder =
578       Builder(packageName, fileName, isScript = false)
579 
580     @JvmStatic public fun scriptBuilder(fileName: String, packageName: String = ""): Builder =
581       Builder(packageName, fileName, isScript = true)
582   }
583 }
584 
585 internal const val DEFAULT_INDENT = "  "
586 
587 private val KOTLIN_DEFAULT_IMPORTS = setOf(
588   "kotlin",
589   "kotlin.annotation",
590   "kotlin.collections",
591   "kotlin.comparisons",
592   "kotlin.io",
593   "kotlin.ranges",
594   "kotlin.sequences",
595   "kotlin.text",
596 )
597 private val KOTLIN_DEFAULT_JVM_IMPORTS = setOf("java.lang")
598 private val KOTLIN_DEFAULT_JS_IMPORTS = setOf("kotlin.js")
599