1 /* <lambda>null2 * Copyright (c) Meta Platforms, Inc. and affiliates. 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 * http://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 17 package com.facebook.ktfmt.format 18 19 import com.facebook.ktfmt.debughelpers.printOps 20 import com.facebook.ktfmt.format.FormattingOptions.Style.DROPBOX 21 import com.facebook.ktfmt.format.FormattingOptions.Style.GOOGLE 22 import com.facebook.ktfmt.format.RedundantElementRemover.dropRedundantElements 23 import com.facebook.ktfmt.format.WhitespaceTombstones.indexOfWhitespaceTombstone 24 import com.facebook.ktfmt.kdoc.Escaping 25 import com.facebook.ktfmt.kdoc.KDocCommentsHelper 26 import com.google.common.collect.ImmutableList 27 import com.google.common.collect.Range 28 import com.google.googlejavaformat.Doc 29 import com.google.googlejavaformat.DocBuilder 30 import com.google.googlejavaformat.Newlines 31 import com.google.googlejavaformat.OpsBuilder 32 import com.google.googlejavaformat.java.FormatterException 33 import com.google.googlejavaformat.java.JavaOutput 34 import org.jetbrains.kotlin.com.intellij.openapi.util.text.StringUtil 35 import org.jetbrains.kotlin.com.intellij.openapi.util.text.StringUtilRt 36 import org.jetbrains.kotlin.com.intellij.psi.PsiComment 37 import org.jetbrains.kotlin.com.intellij.psi.PsiElement 38 import org.jetbrains.kotlin.com.intellij.psi.PsiElementVisitor 39 import org.jetbrains.kotlin.com.intellij.psi.PsiWhiteSpace 40 import org.jetbrains.kotlin.psi.KtImportDirective 41 import org.jetbrains.kotlin.psi.psiUtil.endOffset 42 import org.jetbrains.kotlin.psi.psiUtil.startOffset 43 44 object Formatter { 45 46 @JvmField 47 val GOOGLE_FORMAT = FormattingOptions(style = GOOGLE, blockIndent = 2, continuationIndent = 2) 48 49 /** A format that attempts to reflect https://kotlinlang.org/docs/coding-conventions.html. */ 50 @JvmField 51 val KOTLINLANG_FORMAT = FormattingOptions(style = GOOGLE, blockIndent = 4, continuationIndent = 4) 52 53 @JvmField 54 val DROPBOX_FORMAT = FormattingOptions(style = DROPBOX, blockIndent = 4, continuationIndent = 4) 55 56 private val MINIMUM_KOTLIN_VERSION = KotlinVersion(1, 4) 57 58 /** 59 * format formats the Kotlin code given in 'code' and returns it as a string. This method is 60 * accessed through Reflection. 61 */ 62 @JvmStatic 63 @Throws(FormatterException::class, ParseError::class) 64 fun format(code: String): String = format(FormattingOptions(), code) 65 66 /** 67 * format formats the Kotlin code given in 'code' with 'removeUnusedImports' and returns it as a 68 * string. This method is accessed through Reflection. 69 */ 70 @JvmStatic 71 @Throws(FormatterException::class, ParseError::class) 72 fun format(code: String, removeUnusedImports: Boolean): String = 73 format(FormattingOptions(removeUnusedImports = removeUnusedImports), code) 74 75 /** 76 * format formats the Kotlin code given in 'code' with the 'maxWidth' and returns it as a string. 77 */ 78 @JvmStatic 79 @Throws(FormatterException::class, ParseError::class) 80 fun format(options: FormattingOptions, code: String): String { 81 val (shebang, kotlinCode) = 82 if (code.startsWith("#!")) { 83 code.split("\n".toRegex(), limit = 2) 84 } else { 85 listOf("", code) 86 } 87 checkEscapeSequences(kotlinCode) 88 89 val lfCode = StringUtilRt.convertLineSeparators(kotlinCode) 90 val sortedImports = sortedAndDistinctImports(lfCode) 91 val noRedundantElements = dropRedundantElements(sortedImports, options) 92 val prettyCode = 93 prettyPrint(noRedundantElements, options, Newlines.guessLineSeparator(kotlinCode)!!) 94 return if (shebang.isNotEmpty()) shebang + "\n" + prettyCode else prettyCode 95 } 96 97 /** prettyPrint reflows 'code' using google-java-format's engine. */ 98 private fun prettyPrint(code: String, options: FormattingOptions, lineSeparator: String): String { 99 val file = Parser.parse(code) 100 val kotlinInput = KotlinInput(code, file) 101 val javaOutput = 102 JavaOutput(lineSeparator, kotlinInput, KDocCommentsHelper(lineSeparator, options.maxWidth)) 103 val builder = OpsBuilder(kotlinInput, javaOutput) 104 file.accept(createAstVisitor(options, builder)) 105 builder.sync(kotlinInput.text.length) 106 builder.drain() 107 val ops = builder.build() 108 if (options.debuggingPrintOpsAfterFormatting) { 109 printOps(ops) 110 } 111 val doc = DocBuilder().withOps(ops).build() 112 doc.computeBreaks(javaOutput.commentsHelper, options.maxWidth, Doc.State(+0, 0)) 113 doc.write(javaOutput) 114 javaOutput.flush() 115 116 val tokenRangeSet = 117 kotlinInput.characterRangesToTokenRanges(ImmutableList.of(Range.closedOpen(0, code.length))) 118 return WhitespaceTombstones.replaceTombstoneWithTrailingWhitespace( 119 JavaOutput.applyReplacements(code, javaOutput.getFormatReplacements(tokenRangeSet))) 120 } 121 122 private fun createAstVisitor(options: FormattingOptions, builder: OpsBuilder): PsiElementVisitor { 123 if (KotlinVersion.CURRENT < MINIMUM_KOTLIN_VERSION) { 124 throw RuntimeException("Unsupported runtime Kotlin version: " + KotlinVersion.CURRENT) 125 } 126 return KotlinInputAstVisitor(options, builder) 127 } 128 129 private fun checkEscapeSequences(code: String) { 130 var index = code.indexOfWhitespaceTombstone() 131 if (index == -1) { 132 index = Escaping.indexOfCommentEscapeSequences(code) 133 } 134 if (index != -1) { 135 throw ParseError( 136 "ktfmt does not support code which contains one of {\\u0003, \\u0004, \\u0005} character" + 137 "; escape it", 138 StringUtil.offsetToLineColumn(code, index)) 139 } 140 } 141 142 private fun sortedAndDistinctImports(code: String): String { 143 val file = Parser.parse(code) 144 145 val importList = file.importList ?: return code 146 if (importList.imports.isEmpty()) { 147 return code 148 } 149 150 val commentList = mutableListOf<PsiElement>() 151 // Find non-import elements; comments are moved, in order, to the top of the import list. Other 152 // non-import elements throw a ParseError. 153 var element = importList.firstChild 154 while (element != null) { 155 if (element is PsiComment) { 156 commentList.add(element) 157 } else if (element !is KtImportDirective && element !is PsiWhiteSpace) { 158 throw ParseError( 159 "Imports not contiguous: " + element.text, 160 StringUtil.offsetToLineColumn(code, element.startOffset)) 161 } 162 element = element.nextSibling 163 } 164 fun canonicalText(importDirective: KtImportDirective) = 165 importDirective.importedFqName?.asString() + 166 " " + 167 importDirective.alias?.text?.replace("`", "") + 168 " " + 169 if (importDirective.isAllUnder) "*" else "" 170 171 val sortedImports = importList.imports.sortedBy(::canonicalText).distinctBy(::canonicalText) 172 val importsWithComments = commentList + sortedImports 173 174 return code.replaceRange( 175 importList.startOffset, 176 importList.endOffset, 177 importsWithComments.joinToString(separator = "\n") { imprt -> imprt.text } + "\n") 178 } 179 } 180