blob: c208ad2e888bebac63bb3315fa20a514e1fbd55f [file] [log] [blame]
/*
* Copyright (C) 2015 Square, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.squareup.kotlinpoet
import java.io.Closeable
/** Sentinel value that indicates that no user-provided package has been set. */
private val NO_PACKAGE = String()
internal val NULLABLE_ANY = ANY.copy(nullable = true)
private fun extractMemberName(part: String): String {
require(Character.isJavaIdentifierStart(part[0])) { "not an identifier: $part" }
for (i in 1..part.length) {
if (!part.substring(0, i).isIdentifier) {
return part.substring(0, i - 1)
}
}
return part
}
internal inline fun buildCodeString(builderAction: CodeWriter.() -> Unit): String {
val stringBuilder = StringBuilder()
CodeWriter(stringBuilder, columnLimit = Integer.MAX_VALUE).use {
it.builderAction()
}
return stringBuilder.toString()
}
internal fun buildCodeString(
codeWriter: CodeWriter,
builderAction: CodeWriter.() -> Unit
): String {
val stringBuilder = StringBuilder()
codeWriter.emitInto(stringBuilder, builderAction)
return stringBuilder.toString()
}
/**
* Converts a [FileSpec] to a string suitable to both human- and kotlinc-consumption. This honors
* imports, indentation, and deferred variable names.
*/
internal class CodeWriter constructor(
out: Appendable,
private val indent: String = DEFAULT_INDENT,
private val memberImports: Map<String, Import> = emptyMap(),
val importedTypes: Map<String, ClassName> = emptyMap(),
val importedMembers: Map<String, MemberName> = emptyMap(),
columnLimit: Int = 100
) : Closeable {
private var out = LineWrapper(out, indent, columnLimit)
private var indentLevel = 0
private var kdoc = false
private var comment = false
private var packageName = NO_PACKAGE
private val typeSpecStack = mutableListOf<TypeSpec>()
private val memberImportNames = mutableSetOf<String>()
private val importableTypes = mutableMapOf<String, ClassName>()
private val importableMembers = mutableMapOf<String, MemberName>()
private val referencedNames = mutableSetOf<String>()
private var trailingNewline = false
/**
* When emitting a statement, this is the line of the statement currently being written. The first
* line of a statement is indented normally and subsequent wrapped lines are double-indented. This
* is -1 when the currently-written line isn't part of a statement.
*/
var statementLine = -1
init {
for ((memberName, _) in memberImports) {
val lastDotIndex = memberName.lastIndexOf('.')
if (lastDotIndex >= 0) {
memberImportNames.add(memberName.substring(0, lastDotIndex))
}
}
}
fun indent(levels: Int = 1) = apply {
indentLevel += levels
}
fun unindent(levels: Int = 1) = apply {
require(indentLevel - levels >= 0) { "cannot unindent $levels from $indentLevel" }
indentLevel -= levels
}
fun pushPackage(packageName: String) = apply {
check(this.packageName === NO_PACKAGE) { "package already set: ${this.packageName}" }
this.packageName = packageName
}
fun popPackage() = apply {
check(packageName !== NO_PACKAGE) { "package already set: $packageName" }
packageName = NO_PACKAGE
}
fun pushType(type: TypeSpec) = apply {
this.typeSpecStack.add(type)
}
fun popType() = apply {
this.typeSpecStack.removeAt(typeSpecStack.size - 1)
}
fun emitComment(codeBlock: CodeBlock) {
trailingNewline = true // Force the '//' prefix for the comment.
comment = true
try {
emitCode(codeBlock)
emit("\n")
} finally {
comment = false
}
}
fun emitKdoc(kdocCodeBlock: CodeBlock) {
if (kdocCodeBlock.isEmpty()) return
emit("/**\n")
kdoc = true
try {
emitCode(kdocCodeBlock, ensureTrailingNewline = true)
} finally {
kdoc = false
}
emit(" */\n")
}
fun emitAnnotations(annotations: List<AnnotationSpec>, inline: Boolean) {
for (annotationSpec in annotations) {
annotationSpec.emit(this, inline)
emit(if (inline) " " else "\n")
}
}
/**
* Emits `modifiers` in the standard order. Modifiers in `implicitModifiers` will not
* be emitted except for [KModifier.PUBLIC]
*/
fun emitModifiers(
modifiers: Set<KModifier>,
implicitModifiers: Set<KModifier> = emptySet()
) {
if (shouldEmitPublicModifier(modifiers, implicitModifiers)) {
emit(KModifier.PUBLIC.keyword)
emit(" ")
}
for (modifier in modifiers.toEnumSet()) {
if (modifier in implicitModifiers) continue
emit(modifier.keyword)
emit(" ")
}
}
/**
* Emit type variables with their bounds. If a type variable has more than a single bound - call
* [emitWhereBlock] with same input to produce an additional `where` block.
*
* This should only be used when declaring type variables; everywhere else bounds are omitted.
*/
fun emitTypeVariables(typeVariables: List<TypeVariableName>) {
if (typeVariables.isEmpty()) return
emit("<")
typeVariables.forEachIndexed { index, typeVariable ->
if (index > 0) emit(", ")
if (typeVariable.variance != null) {
emit("${typeVariable.variance.keyword} ")
}
if (typeVariable.isReified) {
emit("reified ")
}
emitCode("%L", typeVariable.name)
if (typeVariable.bounds.size == 1 && typeVariable.bounds[0] != NULLABLE_ANY) {
emitCode(" : %T", typeVariable.bounds[0])
}
}
emit(">")
}
/**
* Emit a `where` block containing type bounds for each type variable that has at least two
* bounds.
*/
fun emitWhereBlock(typeVariables: List<TypeVariableName>) {
if (typeVariables.isEmpty()) return
var firstBound = true
for (typeVariable in typeVariables) {
if (typeVariable.bounds.size > 1) {
for (bound in typeVariable.bounds) {
if (!firstBound) emitCode(", ") else emitCode(" where ")
emitCode("%L : %T", typeVariable.name, bound)
firstBound = false
}
}
}
}
fun emitCode(s: String) = emitCode(CodeBlock.of(s))
fun emitCode(format: String, vararg args: Any?) = emitCode(CodeBlock.of(format, *args))
fun emitCode(
codeBlock: CodeBlock,
isConstantContext: Boolean = false,
ensureTrailingNewline: Boolean = false
) = apply {
var a = 0
var deferredTypeName: ClassName? = null // used by "import static" logic
val partIterator = codeBlock.formatParts.listIterator()
while (partIterator.hasNext()) {
when (val part = partIterator.next()) {
"%L" -> emitLiteral(codeBlock.args[a++], isConstantContext)
"%N" -> emit(codeBlock.args[a++] as String)
"%S" -> {
val string = codeBlock.args[a++] as String?
// Emit null as a literal null: no quotes.
val literal = if (string != null) {
stringLiteralWithQuotes(
string,
escapeDollarSign = true,
isConstantContext = isConstantContext
)
} else {
"null"
}
emit(literal, nonWrapping = true)
}
"%P" -> {
val string = codeBlock.args[a++]?.let { arg ->
if (arg is CodeBlock) {
arg.toString(this@CodeWriter)
} else {
arg as String?
}
}
// Emit null as a literal null: no quotes.
val literal = if (string != null) {
stringLiteralWithQuotes(
string,
escapeDollarSign = false,
isConstantContext = isConstantContext
)
} else {
"null"
}
emit(literal, nonWrapping = true)
}
"%T" -> {
var typeName = codeBlock.args[a++] as TypeName
if (typeName.isAnnotated) {
typeName.emitAnnotations(this)
typeName = typeName.copy(annotations = emptyList())
}
// defer "typeName.emit(this)" if next format part will be handled by the default case
var defer = false
if (typeName is ClassName && partIterator.hasNext()) {
if (!codeBlock.formatParts[partIterator.nextIndex()].startsWith("%")) {
val candidate = typeName
if (candidate.canonicalName in memberImportNames) {
check(deferredTypeName == null) { "pending type for static import?!" }
deferredTypeName = candidate
defer = true
}
}
}
if (!defer) typeName.emit(this)
typeName.emitNullable(this)
}
"%M" -> {
val memberName = codeBlock.args[a++] as MemberName
memberName.emit(this)
}
"%%" -> emit("%")
"⇥" -> indent()
"⇤" -> unindent()
"«" -> {
check(statementLine == -1) {
"""
|Can't open a new statement until the current statement is closed (opening « followed
|by another « without a closing »).
|Current code block:
|- Format parts: ${codeBlock.formatParts.map(::escapeCharacterLiterals)}
|- Arguments: ${codeBlock.args}
|""".trimMargin()
}
statementLine = 0
}
"»" -> {
check(statementLine != -1) {
"""
|Can't close a statement that hasn't been opened (closing » is not preceded by an
|opening «).
|Current code block:
|- Format parts: ${codeBlock.formatParts.map(::escapeCharacterLiterals)}
|- Arguments: ${codeBlock.args}
|""".trimMargin()
}
if (statementLine > 0) {
unindent(2) // End a multi-line statement. Decrease the indentation level.
}
statementLine = -1
}
else -> {
// Handle deferred type.
var doBreak = false
if (deferredTypeName != null) {
if (part.startsWith(".")) {
if (emitStaticImportMember(deferredTypeName.canonicalName, part)) {
// Okay, static import hit and all was emitted, so clean-up and jump to next part.
deferredTypeName = null
doBreak = true
}
}
if (!doBreak) {
deferredTypeName!!.emit(this)
deferredTypeName = null
}
}
if (!doBreak) {
emit(part)
}
}
}
}
if (ensureTrailingNewline && out.hasPendingSegments) {
emit("\n")
}
}
private fun emitStaticImportMember(canonical: String, part: String): Boolean {
val partWithoutLeadingDot = part.substring(1)
if (partWithoutLeadingDot.isEmpty()) return false
val first = partWithoutLeadingDot[0]
if (!Character.isJavaIdentifierStart(first)) return false
val explicit = memberImports[canonical + "." + extractMemberName(partWithoutLeadingDot)]
if (explicit != null) {
if (explicit.alias != null) {
val memberName = extractMemberName(partWithoutLeadingDot)
emit(partWithoutLeadingDot.replaceFirst(memberName, explicit.alias))
} else {
emit(partWithoutLeadingDot)
}
return true
}
return false
}
private fun emitLiteral(o: Any?, isConstantContext: Boolean) {
when (o) {
is TypeSpec -> o.emit(this, null)
is AnnotationSpec -> o.emit(this, inline = true, asParameter = isConstantContext)
is PropertySpec -> o.emit(this, emptySet())
is FunSpec -> o.emit(
codeWriter = this,
enclosingName = null,
implicitModifiers = setOf(KModifier.PUBLIC),
includeKdocTags = true
)
is TypeAliasSpec -> o.emit(this)
is CodeBlock -> emitCode(o, isConstantContext = isConstantContext)
else -> emit(o.toString())
}
}
/**
* Returns the best name to identify `className` with in the current context. This uses the
* available imports and the current scope to find the shortest name available. It does not honor
* names visible due to inheritance.
*/
fun lookupName(className: ClassName): String {
// Find the shortest suffix of className that resolves to className. This uses both local type
// names (so `Entry` in `Map` refers to `Map.Entry`). Also uses imports.
var nameResolved = false
var c: ClassName? = className
while (c != null) {
val alias = memberImports[c.canonicalName]?.alias
val simpleName = alias ?: c.simpleName
val resolved = resolve(simpleName)
nameResolved = resolved != null
// We don't care about nullability and type annotations here, as it's irrelevant for imports.
if (resolved == c.copy(nullable = false, annotations = emptyList())) {
if (alias != null) return alias
val suffixOffset = c.simpleNames.size - 1
referencedNames.add(className.topLevelClassName().simpleName)
return className.simpleNames.subList(
suffixOffset,
className.simpleNames.size
).joinToString(".")
}
c = c.enclosingClassName()
}
// If the name resolved but wasn't a match, we're stuck with the fully qualified name.
if (nameResolved) {
return className.canonicalName
}
// If the class is in the same package, we're done.
if (packageName == className.packageName) {
referencedNames.add(className.topLevelClassName().simpleName)
return className.simpleNames.joinToString(".")
}
// We'll have to use the fully-qualified name. Mark the type as importable for a future pass.
if (!kdoc) {
importableType(className)
}
return className.canonicalName
}
fun lookupName(memberName: MemberName): String {
val simpleName = memberImports[memberName.canonicalName]?.alias ?: memberName.simpleName
// Match an imported member.
val importedMember = importedMembers[simpleName]
if (importedMember == memberName) {
return simpleName
} else if (importedMember != null && memberName.enclosingClassName != null) {
val enclosingClassName = lookupName(memberName.enclosingClassName)
return "$enclosingClassName.$simpleName"
}
// If the member is in the same package, we're done.
if (packageName == memberName.packageName && memberName.enclosingClassName == null) {
referencedNames.add(memberName.simpleName)
return memberName.simpleName
}
// We'll have to use the fully-qualified name.
// Mark the member as importable for a future pass unless the name clashes with
// a method in the current context
if (!kdoc && (
memberName.isExtension ||
!isMethodNameUsedInCurrentContext(memberName.simpleName)
)
) {
importableMember(memberName)
}
return memberName.canonicalName
}
// TODO(luqasn): also honor superclass members when resolving names.
private fun isMethodNameUsedInCurrentContext(simpleName: String): Boolean {
for (it in typeSpecStack.reversed()) {
if (it.funSpecs.any { it.name == simpleName })
return true
if (!it.modifiers.contains(KModifier.INNER))
break
}
return false
}
private fun importableType(className: ClassName) {
val topLevelClassName = className.topLevelClassName()
val simpleName = memberImports[className.canonicalName]?.alias ?: topLevelClassName.simpleName
// Check for name clashes with members.
if (simpleName !in importableMembers) {
importableTypes.putIfAbsent(simpleName, topLevelClassName)
}
}
private fun importableMember(memberName: MemberName) {
if (memberName.packageName.isNotEmpty()) {
val simpleName = memberImports[memberName.canonicalName]?.alias ?: memberName.simpleName
// Check for name clashes with types.
if (simpleName !in importableTypes) {
val namesake = importableMembers.putIfAbsent(simpleName, memberName)
if (namesake != null && memberName.enclosingClassName != null) {
// If there's a name clash and member has an enclosing class, we can mark it as importable
// and use its resolved name later instead of the FQ member name.
importableType(memberName.enclosingClassName)
}
}
}
}
/**
* Returns the class referenced by `simpleName`, using the current nesting context and
* imports.
*/
// TODO(jwilson): also honor superclass members when resolving names.
private fun resolve(simpleName: String): ClassName? {
// Match a child of the current (potentially nested) class.
for (i in typeSpecStack.indices.reversed()) {
val typeSpec = typeSpecStack[i]
if (simpleName in typeSpec.nestedTypesSimpleNames) {
return stackClassName(i, simpleName)
}
}
// Match the top-level class.
if (typeSpecStack.size > 0 && typeSpecStack[0].name == simpleName) {
return ClassName(packageName, simpleName)
}
// Match an imported type.
val importedType = importedTypes[simpleName]
if (importedType != null) return importedType
// No match.
return null
}
/** Returns the class named `simpleName` when nested in the class at `stackDepth`. */
private fun stackClassName(stackDepth: Int, simpleName: String): ClassName {
var className = ClassName(packageName, typeSpecStack[0].name!!)
for (i in 1..stackDepth) {
className = className.nestedClass(typeSpecStack[i].name!!)
}
return className.nestedClass(simpleName)
}
/**
* Emits `s` with indentation as required. It's important that all code that writes to
* [CodeWriter.out] does it through here, since we emit indentation lazily in order to avoid
* unnecessary trailing whitespace.
*/
fun emit(s: String, nonWrapping: Boolean = false) = apply {
var first = true
for (line in s.split('\n')) {
// Emit a newline character. Make sure blank lines in KDoc & comments look good.
if (!first) {
if ((kdoc || comment) && trailingNewline) {
emitIndentation()
out.appendNonWrapping(if (kdoc) " *" else "//")
}
out.newline()
trailingNewline = true
if (statementLine != -1) {
if (statementLine == 0) {
indent(2) // Begin multiple-line statement. Increase the indentation level.
}
statementLine++
}
}
first = false
if (line.isEmpty()) continue // Don't indent empty lines.
// Emit indentation and comment prefix if necessary.
if (trailingNewline) {
emitIndentation()
if (kdoc) {
out.appendNonWrapping(" * ")
} else if (comment) {
out.appendNonWrapping("// ")
}
}
if (nonWrapping) {
out.appendNonWrapping(line)
} else {
out.append(
line,
indentLevel = if (kdoc) indentLevel else indentLevel + 2,
linePrefix = if (kdoc) " * " else ""
)
}
trailingNewline = false
}
}
private fun emitIndentation() {
for (j in 0 until indentLevel) {
out.appendNonWrapping(indent)
}
}
/**
* Returns whether a [KModifier.PUBLIC] should be emitted.
*
* This will return true when [KModifier.PUBLIC] is one of the [implicitModifiers] and
* there are no other opposing modifiers (like [KModifier.PROTECTED] etc.) supplied by the
* consumer.
*/
private fun shouldEmitPublicModifier(
modifiers: Set<KModifier>,
implicitModifiers: Set<KModifier>
): Boolean {
if (modifiers.contains(KModifier.PUBLIC)) {
return true
}
if (!implicitModifiers.contains(KModifier.PUBLIC)) {
return false
}
val containsOtherVisibility =
modifiers.containsAnyOf(KModifier.PRIVATE, KModifier.INTERNAL, KModifier.PROTECTED)
return !containsOtherVisibility
}
/**
* Returns the types that should have been imported for this code. If there were any simple name
* collisions, that type's first use is imported.
*/
fun suggestedTypeImports(): Map<String, ClassName> {
return importableTypes.filterKeys { it !in referencedNames }
}
/**
* Returns the members that should have been imported for this code. If there were any simple name
* collisions, that member's first use is imported.
*/
fun suggestedMemberImports(): Map<String, MemberName> {
return importableMembers.filterKeys { it !in referencedNames }
}
/**
* Perform emitting actions on the current [CodeWriter] using a custom [Appendable]. The
* [CodeWriter] will continue using the old [Appendable] after this method returns.
*/
inline fun emitInto(out: Appendable, action: CodeWriter.() -> Unit) {
val codeWrapper = this
LineWrapper(out, indent = DEFAULT_INDENT, columnLimit = Int.MAX_VALUE).use { newOut ->
val oldOut = codeWrapper.out
codeWrapper.out = newOut
action()
codeWrapper.out = oldOut
}
}
override fun close() {
out.close()
}
}