Skip to content

Commit c69375e

Browse files
committed
Remove redundant curly braces in string templates on inline
1 parent ab1b985 commit c69375e

File tree

8 files changed

+179
-77
lines changed

8 files changed

+179
-77
lines changed

compiler/frontend/src/org/jetbrains/kotlin/psi/KtPsiFactory.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -381,9 +381,9 @@ class KtPsiFactory @JvmOverloads constructor(private val project: Project, val m
381381
return stringTemplateExpression.entries[0] as KtStringTemplateEntryWithExpression
382382
}
383383

384-
fun createSimpleNameStringTemplateEntry(name: String): KtStringTemplateEntryWithExpression {
384+
fun createSimpleNameStringTemplateEntry(name: String): KtSimpleNameStringTemplateEntry {
385385
val stringTemplateExpression = createExpression("\"\$$name\"") as KtStringTemplateExpression
386-
return stringTemplateExpression.entries[0] as KtStringTemplateEntryWithExpression
386+
return stringTemplateExpression.entries[0] as KtSimpleNameStringTemplateEntry
387387
}
388388

389389
fun createStringTemplate(content: String) = createExpression("\"$content\"") as KtStringTemplateExpression

idea/src/org/jetbrains/kotlin/idea/codeInliner/ReplacementPerformer.kt

Lines changed: 54 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,17 @@
1717
package org.jetbrains.kotlin.idea.codeInliner
1818

1919
import com.intellij.openapi.util.Key
20-
import com.intellij.psi.PsiElement
20+
import com.intellij.psi.PsiTreeChangeAdapter
21+
import com.intellij.psi.PsiTreeChangeEvent
2122
import org.jetbrains.kotlin.idea.caches.resolve.analyze
2223
import org.jetbrains.kotlin.idea.core.replaced
2324
import org.jetbrains.kotlin.idea.intentions.ConvertToBlockBodyIntention
25+
import org.jetbrains.kotlin.idea.intentions.RemoveCurlyBracesFromTemplateIntention
2426
import org.jetbrains.kotlin.psi.*
25-
import org.jetbrains.kotlin.psi.psiUtil.*
27+
import org.jetbrains.kotlin.psi.psiUtil.PsiChildRange
28+
import org.jetbrains.kotlin.psi.psiUtil.canPlaceAfterSimpleNameEntry
29+
import org.jetbrains.kotlin.psi.psiUtil.findDescendantOfType
30+
import org.jetbrains.kotlin.psi.psiUtil.parentsWithSelf
2631
import org.jetbrains.kotlin.resolve.bindingContextUtil.isUsedAsExpression
2732
import java.util.*
2833

@@ -109,9 +114,11 @@ internal class ExpressionReplacementPerformer(
109114
}
110115

111116
val mainExpression = codeToInline.mainExpression
112-
val replaced: PsiElement? = when (mainExpression) {
117+
val replaced: KtExpression? = when (mainExpression) {
113118
is KtStringTemplateExpression -> elementToBeReplaced.replacedWithStringTemplate(mainExpression)
119+
114120
is KtExpression -> elementToBeReplaced.replaced(mainExpression)
121+
115122
else -> {
116123
// NB: Unit is never used as expression
117124
val stub = elementToBeReplaced.replaced(psiFactory.createExpression("0"))
@@ -122,7 +129,7 @@ internal class ExpressionReplacementPerformer(
122129
null
123130
}
124131
else {
125-
stub.replace(psiFactory.createExpression("Unit"))
132+
stub.replaced(psiFactory.createExpression("Unit"))
126133
}
127134
}
128135
}
@@ -147,9 +154,28 @@ internal class ExpressionReplacementPerformer(
147154
}
148155
}
149156

150-
range = postProcessing(range)
157+
val listener = replaced?.let { TrackExpressionListener(it) }
158+
listener?.attach()
159+
try {
160+
range = postProcessing(range)
161+
}
162+
finally {
163+
listener?.detach()
164+
}
151165

152-
return range.last as KtExpression? //TODO: return value not correct!
166+
val resultExpression = listener?.result
167+
168+
// simplify "${x}" to "$x"
169+
val templateEntry = resultExpression?.parent as? KtBlockStringTemplateEntry
170+
if (templateEntry != null) {
171+
val intention = RemoveCurlyBracesFromTemplateIntention()
172+
if (intention.isApplicableTo(templateEntry)) {
173+
val newEntry = intention.applyTo(templateEntry)
174+
return newEntry.expression
175+
}
176+
}
177+
178+
return resultExpression ?: range.last as? KtExpression
153179
}
154180

155181
/**
@@ -204,6 +230,28 @@ internal class ExpressionReplacementPerformer(
204230
elementToBeReplaced.putCopyableUserData(ELEMENT_TO_BE_REPLACED_KEY, null)
205231
return result
206232
}
233+
234+
private class TrackExpressionListener(expression: KtExpression) : PsiTreeChangeAdapter() {
235+
private var expression: KtExpression? = expression
236+
private val manager = expression.manager
237+
238+
fun attach() {
239+
manager.addPsiTreeChangeListener(this)
240+
}
241+
242+
fun detach() {
243+
manager.removePsiTreeChangeListener(this)
244+
}
245+
246+
val result: KtExpression?
247+
get() = expression?.takeIf { it.isValid }
248+
249+
override fun childReplaced(event: PsiTreeChangeEvent) {
250+
if (event.oldChild == expression) {
251+
expression = event.newChild as? KtExpression
252+
}
253+
}
254+
}
207255
}
208256

209257
private val ELEMENT_TO_BE_REPLACED_KEY = Key<Unit>("ELEMENT_TO_BE_REPLACED_KEY")

idea/src/org/jetbrains/kotlin/idea/codeInliner/UsageReplacementStrategy.kt

Lines changed: 88 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import com.intellij.openapi.progress.ProgressIndicator
2222
import com.intellij.openapi.progress.ProgressManager
2323
import com.intellij.openapi.progress.Task
2424
import com.intellij.openapi.project.Project
25+
import com.intellij.openapi.util.Key
2526
import com.intellij.psi.PsiElement
2627
import com.intellij.psi.search.GlobalSearchScope
2728
import com.intellij.psi.search.searches.ReferencesSearch
@@ -39,6 +40,7 @@ import org.jetbrains.kotlin.idea.stubindex.KotlinSourceFilterScope
3940
import org.jetbrains.kotlin.idea.util.application.executeWriteCommand
4041
import org.jetbrains.kotlin.idea.util.application.runReadAction
4142
import org.jetbrains.kotlin.psi.*
43+
import org.jetbrains.kotlin.psi.psiUtil.collectDescendantsOfType
4244
import org.jetbrains.kotlin.psi.psiUtil.forEachDescendantOfType
4345
import org.jetbrains.kotlin.psi.psiUtil.getStrictParentOfType
4446
import org.jetbrains.kotlin.resolve.BindingContext
@@ -116,84 +118,104 @@ fun UsageReplacementStrategy.replaceUsages(
116118
) {
117119
GuiUtils.invokeLaterIfNeeded({
118120
project.executeWriteCommand(commandName) {
119-
// we should delete imports later to not affect other usages
120-
val importsToDelete = arrayListOf<KtImportDirective>()
121-
val replacements = mutableListOf<KtElement>()
122-
123-
var invalidUsagesFound = false
124-
125121
val targetDeclaration = targetPsiElement as? KtNamedDeclaration
126122

127-
val usagesChildrenFirst = usages.sortChildrenFirst()
128-
usages@ for (usage in usagesChildrenFirst) {
129-
try {
130-
if (!usage.isValid) {
131-
invalidUsagesFound = true
132-
continue
133-
}
123+
val usagesByFile = usages.groupBy { it.containingFile }
134124

135-
val usageParent = usage.parent
136-
when (usageParent) {
137-
is KtCallableReferenceExpression -> {
138-
val grandParent = usageParent.parent
139-
ConvertReferenceToLambdaIntention().applyTo(usageParent, null)
140-
(grandParent as? KtElement)?.let {
141-
doRefactoringInside(it, targetDeclaration?.name, targetDeclaration?.descriptor)
142-
}
143-
continue@usages
144-
}
145-
is KtCallElement -> {
146-
val lambdaArguments = usageParent.lambdaArguments
147-
if (lambdaArguments.isNotEmpty()) {
148-
val grandParent = usageParent.parent
149-
val specifySignature = SpecifyExplicitLambdaSignatureIntention()
150-
for (lambdaArgument in lambdaArguments) {
151-
val lambdaExpression = lambdaArgument.getLambdaExpression()
152-
val functionDescriptor =
153-
lambdaExpression.functionLiteral.resolveToDescriptorIfAny() as? FunctionDescriptor ?: continue
154-
if (functionDescriptor.valueParameters.isNotEmpty()) {
155-
specifySignature.applyTo(lambdaExpression, null)
156-
}
157-
}
158-
(grandParent as? KtElement)?.let {
159-
doRefactoringInside(it, targetDeclaration?.name, targetDeclaration?.descriptor)
160-
}
161-
continue@usages
162-
}
163-
}
125+
val KEY = Key<Unit>("UsageReplacementStrategy.replaceUsages")
164126

165-
}
127+
for ((file, usagesInFile) in usagesByFile) {
128+
usagesInFile.forEach { it.putCopyableUserData(KEY, Unit) }
166129

167-
//TODO: keep the import if we don't know how to replace some of the usages
168-
val importDirective = usage.getStrictParentOfType<KtImportDirective>()
169-
if (importDirective != null) {
170-
if (!importDirective.isAllUnder && importDirective.targetDescriptors().size == 1) {
171-
importsToDelete.add(importDirective)
172-
}
173-
continue
174-
}
130+
// we should delete imports later to not affect other usages
131+
val importsToDelete = mutableListOf<KtImportDirective>()
175132

176-
val replacement = createReplacer(usage)?.invoke()
177-
if (replacement != null) {
178-
replacements += replacement
179-
}
180-
}
181-
catch (e: Throwable) {
182-
LOG.error(e)
133+
var usagesToProcess = usagesInFile
134+
while (usagesToProcess.isNotEmpty()) {
135+
if (processUsages(usagesToProcess, targetDeclaration, importsToDelete)) break
136+
137+
// some usages may get invalidated we need to find them in the tree
138+
usagesToProcess = file.collectDescendantsOfType<KtSimpleNameExpression> { it.getCopyableUserData(KEY) != null }
183139
}
140+
141+
file.forEachDescendantOfType<KtSimpleNameExpression> { it.putCopyableUserData(KEY, null) }
142+
143+
importsToDelete.forEach { it.delete() }
144+
}
145+
146+
postAction()
147+
}
148+
}, ModalityState.NON_MODAL)
149+
}
150+
151+
/**
152+
* @return false if some usages were invalidated
153+
*/
154+
private fun UsageReplacementStrategy.processUsages(
155+
usages: List<KtSimpleNameExpression>,
156+
targetDeclaration: KtNamedDeclaration?,
157+
importsToDelete: MutableList<KtImportDirective>
158+
): Boolean {
159+
var invalidUsagesFound = false
160+
for (usage in usages.sortChildrenFirst()) {
161+
try {
162+
if (!usage.isValid) {
163+
invalidUsagesFound = true
164+
continue
184165
}
185166

186-
if (invalidUsagesFound && targetDeclaration != null) {
187-
val name = targetDeclaration.name
188-
val descriptor = targetDeclaration.descriptor
189-
for (replacement in replacements) {
190-
doRefactoringInside(replacement, name, descriptor)
167+
if (specialUsageProcessing(usage, targetDeclaration)) continue
168+
169+
//TODO: keep the import if we don't know how to replace some of the usages
170+
val importDirective = usage.getStrictParentOfType<KtImportDirective>()
171+
if (importDirective != null) {
172+
if (!importDirective.isAllUnder && importDirective.targetDescriptors().size == 1) {
173+
importsToDelete.add(importDirective)
191174
}
175+
continue
192176
}
193177

194-
importsToDelete.forEach { it.delete() }
178+
createReplacer(usage)?.invoke()
179+
}
180+
catch (e: Throwable) {
181+
LOG.error(e)
182+
}
183+
}
184+
return !invalidUsagesFound
185+
}
195186

196-
postAction()
187+
private fun UsageReplacementStrategy.specialUsageProcessing(usage: KtSimpleNameExpression, targetDeclaration: KtNamedDeclaration?): Boolean {
188+
val usageParent = usage.parent
189+
when (usageParent) {
190+
is KtCallableReferenceExpression -> {
191+
val grandParent = usageParent.parent
192+
ConvertReferenceToLambdaIntention().applyTo(usageParent, null)
193+
(grandParent as? KtElement)?.let {
194+
doRefactoringInside(it, targetDeclaration?.name, targetDeclaration?.descriptor)
195+
}
196+
return true
197197
}
198-
}, ModalityState.NON_MODAL)
198+
199+
is KtCallElement -> {
200+
val lambdaArguments = usageParent.lambdaArguments
201+
if (lambdaArguments.isNotEmpty()) {
202+
val grandParent = usageParent.parent
203+
val specifySignature = SpecifyExplicitLambdaSignatureIntention()
204+
for (lambdaArgument in lambdaArguments) {
205+
val lambdaExpression = lambdaArgument.getLambdaExpression()
206+
val functionDescriptor =
207+
lambdaExpression.functionLiteral.resolveToDescriptorIfAny() as? FunctionDescriptor ?: continue
208+
if (functionDescriptor.valueParameters.isNotEmpty()) {
209+
specifySignature.applyTo(lambdaExpression, null)
210+
}
211+
}
212+
(grandParent as? KtElement)?.let {
213+
doRefactoringInside(it, targetDeclaration?.name, targetDeclaration?.descriptor)
214+
}
215+
return true
216+
}
217+
}
218+
219+
}
220+
return false
199221
}

idea/src/org/jetbrains/kotlin/idea/intentions/RemoveCurlyBracesFromTemplateIntention.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ import org.jetbrains.kotlin.idea.inspections.IntentionBasedInspection
2222
import org.jetbrains.kotlin.psi.KtBlockStringTemplateEntry
2323
import org.jetbrains.kotlin.psi.KtNameReferenceExpression
2424
import org.jetbrains.kotlin.psi.KtPsiFactory
25-
import org.jetbrains.kotlin.psi.KtStringTemplateEntryWithExpression
25+
import org.jetbrains.kotlin.psi.KtSimpleNameStringTemplateEntry
2626
import org.jetbrains.kotlin.psi.psiUtil.canPlaceAfterSimpleNameEntry
2727

2828
class RemoveCurlyBracesFromTemplateInspection : IntentionBasedInspection<KtBlockStringTemplateEntry>(RemoveCurlyBracesFromTemplateIntention::class)
@@ -37,7 +37,7 @@ class RemoveCurlyBracesFromTemplateIntention : SelfTargetingOffsetIndependentInt
3737
applyTo(element)
3838
}
3939

40-
fun applyTo(element: KtBlockStringTemplateEntry): KtStringTemplateEntryWithExpression {
40+
fun applyTo(element: KtBlockStringTemplateEntry): KtSimpleNameStringTemplateEntry {
4141
val name = (element.expression as KtNameReferenceExpression).getReferencedName()
4242
val newEntry = KtPsiFactory(element).createSimpleNameStringTemplateEntry(name)
4343
return element.replaced(newEntry)

idea/testData/quickfix/deprecatedSymbolUsage/argumentSideEffects/complexExpressionNotUsed4Runtime.kt.after

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ fun newFun(): Int = 0
1010

1111
fun foo(): Int {
1212
bar()
13-
<caret>return newFun()
13+
return <caret>newFun()
1414
}
1515

1616
fun bar(): Int? = 0
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
class X(val v: Int) {
2+
fun <caret>foo() {
3+
println("foo()")
4+
return v
5+
}
6+
}
7+
8+
fun X.f1() {
9+
println("Value: ${foo()}")
10+
}
11+
12+
fun f2(x: X) {
13+
println("Value: ${x.foo()}")
14+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
class X(val v: Int) {
2+
}
3+
4+
fun X.f1() {
5+
println("foo()")
6+
println("Value: $v")
7+
}
8+
9+
fun f2(x: X) {
10+
println("foo()")
11+
println("Value: ${x.v}")
12+
}

idea/tests/org/jetbrains/kotlin/idea/refactoring/inline/InlineTestGenerated.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,12 @@ public void testEmptyFunction() throws Exception {
5050
doTest(fileName);
5151
}
5252

53+
@TestMetadata("InStringTemplates.kt")
54+
public void testInStringTemplates() throws Exception {
55+
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/refactoring/inline/function/InStringTemplates.kt");
56+
doTest(fileName);
57+
}
58+
5359
@TestMetadata("LocalCapturing.kt")
5460
public void testLocalCapturing() throws Exception {
5561
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/refactoring/inline/function/LocalCapturing.kt");

0 commit comments

Comments
 (0)