xref: /aosp_15_r20/external/ktfmt/core/src/main/java/com/facebook/ktfmt/format/TrailingCommas.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.PsiComment
20 import org.jetbrains.kotlin.com.intellij.psi.PsiElement
21 import org.jetbrains.kotlin.com.intellij.psi.PsiWhiteSpace
22 import org.jetbrains.kotlin.psi.KtClassBody
23 import org.jetbrains.kotlin.psi.KtCollectionLiteralExpression
24 import org.jetbrains.kotlin.psi.KtElement
25 import org.jetbrains.kotlin.psi.KtEnumEntry
26 import org.jetbrains.kotlin.psi.KtFunctionLiteral
27 import org.jetbrains.kotlin.psi.KtLambdaExpression
28 import org.jetbrains.kotlin.psi.KtParameterList
29 import org.jetbrains.kotlin.psi.KtTypeArgumentList
30 import org.jetbrains.kotlin.psi.KtTypeParameterList
31 import org.jetbrains.kotlin.psi.KtValueArgumentList
32 import org.jetbrains.kotlin.psi.KtWhenEntry
33 
34 /** Detects trailing commas or elements that should have trailing commas. */
35 object TrailingCommas {
36 
37   class Detector {
38     private val trailingCommas = mutableListOf<PsiElement>()
39 
getTrailingCommaElementsnull40     fun getTrailingCommaElements(): List<PsiElement> = trailingCommas
41 
42     /** returns **true** if this element was a traling comma, **false** otherwise. */
43     fun takeElement(element: PsiElement) {
44       if (isTrailingComma(element)) {
45         trailingCommas += element
46       }
47     }
48 
isTrailingCommanull49     private fun isTrailingComma(element: PsiElement): Boolean {
50       if (element.text != ",") {
51         return false
52       }
53 
54       return extractManagedList(element.parent)?.trailingComma == element
55     }
56   }
57 
58   class Suggestor {
59     private val suggestionElements = mutableListOf<PsiElement>()
60 
getTrailingCommaSuggestionsnull61     fun getTrailingCommaSuggestions(): List<PsiElement> = suggestionElements
62 
63     /**
64      * Record elements which should have trailing commas inserted.
65      *
66      * This function determines which element type which may need trailing commas, as well as logic
67      * for when they shold be inserted.
68      *
69      * Example:
70      * ```
71      * fun foo(
72      *   x: VeryLongName,
73      *   y: MoreThanLineLimit // Record this list
74      * ) { }
75      *
76      * fun bar(x: ShortName, y: FitsOnLine) { } // Ignore this list
77      * ```
78      */
79     fun takeElement(element: KtElement) {
80       if (!element.text.contains("\n")) {
81         return // Only suggest trailing commas where there is already a line break
82       }
83 
84       when (element) {
85         is KtEnumEntry, // Only suggest on the KtClassBody container
86         is KtWhenEntry -> return
87         is KtParameterList -> {
88           if (element.parent is KtFunctionLiteral && element.parent.parent is KtLambdaExpression) {
89             return // Never add trailing commas to lambda param lists
90           }
91         }
92         is KtClassBody -> {
93           EnumEntryList.extractChildList(element)?.also {
94             if (it.terminatingSemicolon != null) {
95               return // Never add a trailing comma after there is already a terminating semicolon
96             }
97           }
98         }
99       }
100 
101       val list = extractManagedList(element) ?: return
102       if (list.items.size <= 1) {
103         return // Never insert commas to single-element lists
104       }
105       if (list.trailingComma != null) {
106         return // Never insert a comma if there already is one somehow
107       }
108 
109       suggestionElements.add(list.items.last().leftLeafIgnoringCommentsAndWhitespace())
110     }
111   }
112 
113   private class ManagedList(val items: List<KtElement>, val trailingComma: PsiElement?)
114 
extractManagedListnull115   private fun extractManagedList(element: PsiElement): ManagedList? {
116     return when (element) {
117       is KtValueArgumentList -> ManagedList(element.arguments, element.trailingComma)
118       is KtParameterList -> ManagedList(element.parameters, element.trailingComma)
119       is KtTypeArgumentList -> ManagedList(element.arguments, element.trailingComma)
120       is KtTypeParameterList -> ManagedList(element.parameters, element.trailingComma)
121       is KtCollectionLiteralExpression -> {
122         ManagedList(element.getInnerExpressions(), element.trailingComma)
123       }
124       is KtWhenEntry -> ManagedList(element.conditions.toList(), element.trailingComma)
125       is KtEnumEntry -> {
126         EnumEntryList.extractParentList(element).let {
127           ManagedList(it.enumEntries, it.trailingComma)
128         }
129       }
130       is KtClassBody -> {
131         EnumEntryList.extractChildList(element)?.let {
132           ManagedList(it.enumEntries, it.trailingComma)
133         }
134       }
135       else -> null
136     }
137   }
138 
139   /**
140    * Return the element ahead of the where a comma would be appropriate for a list item.
141    *
142    * Example:
143    * ```
144    * fun foo(
145    *   x: VeryLongName,
146    *   y: MoreThanLineLimit /# Comment #/ = { it } /# Comment #/
147    *                                         ^^^^^^ // After this element
148    * ) { }
149    * ```
150    */
leftLeafIgnoringCommentsAndWhitespacenull151   private fun PsiElement.leftLeafIgnoringCommentsAndWhitespace(): PsiElement {
152     var child = this.lastChild
153     while (child != null) {
154       if (child is PsiWhiteSpace || child is PsiComment) {
155         child = child.prevSibling
156       } else {
157         return child.leftLeafIgnoringCommentsAndWhitespace()
158       }
159     }
160     return this
161   }
162 }
163