blob: 1fa90f75b6f9bf61e376a430c7a3f4278a977d31 [file] [log] [blame]
/*
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* 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
*
* http://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.facebook.ktfmt.format
import com.facebook.ktfmt.debughelpers.printOps
import com.facebook.ktfmt.format.FormattingOptions.Style.DROPBOX
import com.facebook.ktfmt.format.FormattingOptions.Style.GOOGLE
import com.facebook.ktfmt.format.RedundantElementRemover.dropRedundantElements
import com.facebook.ktfmt.format.WhitespaceTombstones.indexOfWhitespaceTombstone
import com.facebook.ktfmt.kdoc.Escaping
import com.facebook.ktfmt.kdoc.KDocCommentsHelper
import com.google.common.collect.ImmutableList
import com.google.common.collect.Range
import com.google.googlejavaformat.Doc
import com.google.googlejavaformat.DocBuilder
import com.google.googlejavaformat.Newlines
import com.google.googlejavaformat.OpsBuilder
import com.google.googlejavaformat.java.FormatterException
import com.google.googlejavaformat.java.JavaOutput
import org.jetbrains.kotlin.com.intellij.openapi.util.text.StringUtil
import org.jetbrains.kotlin.com.intellij.openapi.util.text.StringUtilRt
import org.jetbrains.kotlin.com.intellij.psi.PsiComment
import org.jetbrains.kotlin.com.intellij.psi.PsiElement
import org.jetbrains.kotlin.com.intellij.psi.PsiElementVisitor
import org.jetbrains.kotlin.com.intellij.psi.PsiWhiteSpace
import org.jetbrains.kotlin.psi.KtImportDirective
import org.jetbrains.kotlin.psi.psiUtil.endOffset
import org.jetbrains.kotlin.psi.psiUtil.startOffset
object Formatter {
@JvmField
val GOOGLE_FORMAT = FormattingOptions(style = GOOGLE, blockIndent = 2, continuationIndent = 2)
/** A format that attempts to reflect https://kotlinlang.org/docs/coding-conventions.html. */
@JvmField
val KOTLINLANG_FORMAT = FormattingOptions(style = GOOGLE, blockIndent = 4, continuationIndent = 4)
@JvmField
val DROPBOX_FORMAT = FormattingOptions(style = DROPBOX, blockIndent = 4, continuationIndent = 4)
private val MINIMUM_KOTLIN_VERSION = KotlinVersion(1, 4)
/**
* format formats the Kotlin code given in 'code' and returns it as a string. This method is
* accessed through Reflection.
*/
@JvmStatic
@Throws(FormatterException::class, ParseError::class)
fun format(code: String): String = format(FormattingOptions(), code)
/**
* format formats the Kotlin code given in 'code' with 'removeUnusedImports' and returns it as a
* string. This method is accessed through Reflection.
*/
@JvmStatic
@Throws(FormatterException::class, ParseError::class)
fun format(code: String, removeUnusedImports: Boolean): String =
format(FormattingOptions(removeUnusedImports = removeUnusedImports), code)
/**
* format formats the Kotlin code given in 'code' with the 'maxWidth' and returns it as a string.
*/
@JvmStatic
@Throws(FormatterException::class, ParseError::class)
fun format(options: FormattingOptions, code: String): String {
val (shebang, kotlinCode) =
if (code.startsWith("#!")) {
code.split("\n".toRegex(), limit = 2)
} else {
listOf("", code)
}
checkEscapeSequences(kotlinCode)
val lfCode = StringUtilRt.convertLineSeparators(kotlinCode)
val sortedImports = sortedAndDistinctImports(lfCode)
val pretty = prettyPrint(sortedImports, options, "\n")
val noRedundantElements =
try {
dropRedundantElements(pretty, options)
} catch (e: ParseError) {
throw IllegalStateException("Failed to re-parse code after pretty printing:\n $pretty", e)
}
val prettyCode =
prettyPrint(noRedundantElements, options, Newlines.guessLineSeparator(kotlinCode)!!)
return if (shebang.isNotEmpty()) shebang + "\n" + prettyCode else prettyCode
}
/** prettyPrint reflows 'code' using google-java-format's engine. */
private fun prettyPrint(code: String, options: FormattingOptions, lineSeparator: String): String {
val file = Parser.parse(code)
val kotlinInput = KotlinInput(code, file)
val javaOutput =
JavaOutput(lineSeparator, kotlinInput, KDocCommentsHelper(lineSeparator, options.maxWidth))
val builder = OpsBuilder(kotlinInput, javaOutput)
file.accept(createAstVisitor(options, builder))
builder.sync(kotlinInput.text.length)
builder.drain()
val ops = builder.build()
if (options.debuggingPrintOpsAfterFormatting) {
printOps(ops)
}
val doc = DocBuilder().withOps(ops).build()
doc.computeBreaks(javaOutput.commentsHelper, options.maxWidth, Doc.State(+0, 0))
doc.write(javaOutput)
javaOutput.flush()
val tokenRangeSet =
kotlinInput.characterRangesToTokenRanges(ImmutableList.of(Range.closedOpen(0, code.length)))
return WhitespaceTombstones.replaceTombstoneWithTrailingWhitespace(
JavaOutput.applyReplacements(code, javaOutput.getFormatReplacements(tokenRangeSet)))
}
private fun createAstVisitor(options: FormattingOptions, builder: OpsBuilder): PsiElementVisitor {
if (KotlinVersion.CURRENT < MINIMUM_KOTLIN_VERSION) {
throw RuntimeException("Unsupported runtime Kotlin version: " + KotlinVersion.CURRENT)
}
return KotlinInputAstVisitor(options, builder)
}
private fun checkEscapeSequences(code: String) {
var index = code.indexOfWhitespaceTombstone()
if (index == -1) {
index = Escaping.indexOfCommentEscapeSequences(code)
}
if (index != -1) {
throw ParseError(
"ktfmt does not support code which contains one of {\\u0003, \\u0004, \\u0005} character" +
"; escape it",
StringUtil.offsetToLineColumn(code, index))
}
}
private fun sortedAndDistinctImports(code: String): String {
val file = Parser.parse(code)
val importList = file.importList ?: return code
if (importList.imports.isEmpty()) {
return code
}
val commentList = mutableListOf<PsiElement>()
// Find non-import elements; comments are moved, in order, to the top of the import list. Other
// non-import elements throw a ParseError.
var element = importList.firstChild
while (element != null) {
if (element is PsiComment) {
commentList.add(element)
} else if (element !is KtImportDirective && element !is PsiWhiteSpace) {
throw ParseError(
"Imports not contiguous: " + element.text,
StringUtil.offsetToLineColumn(code, element.startOffset))
}
element = element.nextSibling
}
fun canonicalText(importDirective: KtImportDirective) =
importDirective.importedFqName?.asString() +
" " +
importDirective.alias?.text?.replace("`", "") +
" " +
if (importDirective.isAllUnder) "*" else ""
val sortedImports = importList.imports.sortedBy(::canonicalText).distinctBy(::canonicalText)
val importsWithComments = commentList + sortedImports
return code.replaceRange(
importList.startOffset,
importList.endOffset,
importsWithComments.joinToString(separator = "\n") { imprt -> imprt.text } + "\n")
}
}