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