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