xref: /aosp_15_r20/external/ktfmt/core/src/main/java/com/facebook/ktfmt/format/Formatter.kt (revision 5be3f65c8cf0e6db0a7e312df5006e8e93cdf9ec)
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