• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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