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