xref: /aosp_15_r20/external/ktfmt/core/src/main/java/com/facebook/ktfmt/format/RedundantElementManager.kt (revision 5be3f65c8cf0e6db0a7e312df5006e8e93cdf9ec)
1 /*
2  * 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 org.jetbrains.kotlin.com.intellij.psi.PsiElement
20 import org.jetbrains.kotlin.com.intellij.psi.PsiWhiteSpace
21 import org.jetbrains.kotlin.kdoc.psi.impl.KDocImpl
22 import org.jetbrains.kotlin.psi.KtElement
23 import org.jetbrains.kotlin.psi.KtImportList
24 import org.jetbrains.kotlin.psi.KtPackageDirective
25 import org.jetbrains.kotlin.psi.KtReferenceExpression
26 import org.jetbrains.kotlin.psi.KtTreeVisitorVoid
27 import org.jetbrains.kotlin.psi.psiUtil.endOffset
28 import org.jetbrains.kotlin.psi.psiUtil.startOffset
29 
30 /**
31  * Adds and removes elements that are not strictly needed in the code, such as semicolons and unused
32  * imports.
33  */
34 object RedundantElementManager {
35   /** Remove extra semicolons and unused imports, if enabled in the [options] */
dropRedundantElementsnull36   fun dropRedundantElements(code: String, options: FormattingOptions): String {
37     val file = Parser.parse(code)
38     val redundantImportDetector = RedundantImportDetector(enabled = options.removeUnusedImports)
39     val redundantSemicolonDetector = RedundantSemicolonDetector()
40     val trailingCommaDetector = TrailingCommas.Detector()
41 
42     file.accept(
43         object : KtTreeVisitorVoid() {
44           override fun visitElement(element: PsiElement) {
45             if (element is KDocImpl) {
46               redundantImportDetector.takeKdoc(element)
47               return
48             }
49 
50             redundantSemicolonDetector.takeElement(element)
51             if (options.manageTrailingCommas) {
52               trailingCommaDetector.takeElement(element)
53             }
54             super.visitElement(element)
55           }
56 
57           override fun visitPackageDirective(directive: KtPackageDirective) {
58             redundantImportDetector.takePackageDirective(directive) {
59               super.visitPackageDirective(directive)
60             }
61           }
62 
63           override fun visitImportList(importList: KtImportList) {
64             redundantImportDetector.takeImportList(importList) { super.visitImportList(importList) }
65           }
66 
67           override fun visitReferenceExpression(expression: KtReferenceExpression) {
68             redundantImportDetector.takeReferenceExpression(expression)
69             super.visitReferenceExpression(expression)
70           }
71         })
72 
73     val result = StringBuilder(code)
74     val elementsToRemove =
75         redundantSemicolonDetector.getRedundantSemicolonElements() +
76             redundantImportDetector.getRedundantImportElements() +
77             trailingCommaDetector.getTrailingCommaElements()
78 
79     for (element in elementsToRemove.sortedByDescending(PsiElement::endOffset)) {
80       // Don't insert extra newlines when the semicolon is already a line terminator
81       val replacement =
82           if (element.text == ";" && !element.nextSibling.containsNewline()) {
83             "\n"
84           } else {
85             ""
86           }
87       result.replace(element.startOffset, element.endOffset, replacement)
88     }
89 
90     return result.toString()
91   }
92 
addRedundantElementsnull93   fun addRedundantElements(code: String, options: FormattingOptions): String {
94     if (!options.manageTrailingCommas) {
95       return code
96     }
97 
98     val file = Parser.parse(code)
99     val trailingCommaSuggestor = TrailingCommas.Suggestor()
100 
101     file.accept(
102         object : KtTreeVisitorVoid() {
103           override fun visitKtElement(element: KtElement) {
104             trailingCommaSuggestor.takeElement(element)
105             super.visitElement(element)
106           }
107         })
108 
109     val result = StringBuilder(code)
110     val suggestionElements = trailingCommaSuggestor.getTrailingCommaSuggestions()
111 
112     for (element in suggestionElements.sortedByDescending(PsiElement::endOffset)) {
113       result.insert(element.endOffset, ',')
114     }
115 
116     return result.toString()
117   }
118 
containsNewlinenull119   private fun PsiElement?.containsNewline(): Boolean {
120     if (this !is PsiWhiteSpace) return false
121     return this.text.contains('\n')
122   }
123 }
124