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