Remove redundant curly braces in string templates on inline

This commit is contained in:
Valentin Kipyatkov
2017-05-23 17:13:02 +03:00
parent ab1b985bac
commit c69375ebdc
8 changed files with 181 additions and 79 deletions
@@ -381,9 +381,9 @@ class KtPsiFactory @JvmOverloads constructor(private val project: Project, val m
return stringTemplateExpression.entries[0] as KtStringTemplateEntryWithExpression
}
fun createSimpleNameStringTemplateEntry(name: String): KtStringTemplateEntryWithExpression {
fun createSimpleNameStringTemplateEntry(name: String): KtSimpleNameStringTemplateEntry {
val stringTemplateExpression = createExpression("\"\$$name\"") as KtStringTemplateExpression
return stringTemplateExpression.entries[0] as KtStringTemplateEntryWithExpression
return stringTemplateExpression.entries[0] as KtSimpleNameStringTemplateEntry
}
fun createStringTemplate(content: String) = createExpression("\"$content\"") as KtStringTemplateExpression
@@ -17,12 +17,17 @@
package org.jetbrains.kotlin.idea.codeInliner
import com.intellij.openapi.util.Key
import com.intellij.psi.PsiElement
import com.intellij.psi.PsiTreeChangeAdapter
import com.intellij.psi.PsiTreeChangeEvent
import org.jetbrains.kotlin.idea.caches.resolve.analyze
import org.jetbrains.kotlin.idea.core.replaced
import org.jetbrains.kotlin.idea.intentions.ConvertToBlockBodyIntention
import org.jetbrains.kotlin.idea.intentions.RemoveCurlyBracesFromTemplateIntention
import org.jetbrains.kotlin.psi.*
import org.jetbrains.kotlin.psi.psiUtil.*
import org.jetbrains.kotlin.psi.psiUtil.PsiChildRange
import org.jetbrains.kotlin.psi.psiUtil.canPlaceAfterSimpleNameEntry
import org.jetbrains.kotlin.psi.psiUtil.findDescendantOfType
import org.jetbrains.kotlin.psi.psiUtil.parentsWithSelf
import org.jetbrains.kotlin.resolve.bindingContextUtil.isUsedAsExpression
import java.util.*
@@ -109,9 +114,11 @@ internal class ExpressionReplacementPerformer(
}
val mainExpression = codeToInline.mainExpression
val replaced: PsiElement? = when (mainExpression) {
val replaced: KtExpression? = when (mainExpression) {
is KtStringTemplateExpression -> elementToBeReplaced.replacedWithStringTemplate(mainExpression)
is KtExpression -> elementToBeReplaced.replaced(mainExpression)
else -> {
// NB: Unit is never used as expression
val stub = elementToBeReplaced.replaced(psiFactory.createExpression("0"))
@@ -122,7 +129,7 @@ internal class ExpressionReplacementPerformer(
null
}
else {
stub.replace(psiFactory.createExpression("Unit"))
stub.replaced(psiFactory.createExpression("Unit"))
}
}
}
@@ -147,9 +154,28 @@ internal class ExpressionReplacementPerformer(
}
}
range = postProcessing(range)
val listener = replaced?.let { TrackExpressionListener(it) }
listener?.attach()
try {
range = postProcessing(range)
}
finally {
listener?.detach()
}
return range.last as KtExpression? //TODO: return value not correct!
val resultExpression = listener?.result
// simplify "${x}" to "$x"
val templateEntry = resultExpression?.parent as? KtBlockStringTemplateEntry
if (templateEntry != null) {
val intention = RemoveCurlyBracesFromTemplateIntention()
if (intention.isApplicableTo(templateEntry)) {
val newEntry = intention.applyTo(templateEntry)
return newEntry.expression
}
}
return resultExpression ?: range.last as? KtExpression
}
/**
@@ -204,6 +230,28 @@ internal class ExpressionReplacementPerformer(
elementToBeReplaced.putCopyableUserData(ELEMENT_TO_BE_REPLACED_KEY, null)
return result
}
private class TrackExpressionListener(expression: KtExpression) : PsiTreeChangeAdapter() {
private var expression: KtExpression? = expression
private val manager = expression.manager
fun attach() {
manager.addPsiTreeChangeListener(this)
}
fun detach() {
manager.removePsiTreeChangeListener(this)
}
val result: KtExpression?
get() = expression?.takeIf { it.isValid }
override fun childReplaced(event: PsiTreeChangeEvent) {
if (event.oldChild == expression) {
expression = event.newChild as? KtExpression
}
}
}
}
private val ELEMENT_TO_BE_REPLACED_KEY = Key<Unit>("ELEMENT_TO_BE_REPLACED_KEY")
@@ -22,6 +22,7 @@ import com.intellij.openapi.progress.ProgressIndicator
import com.intellij.openapi.progress.ProgressManager
import com.intellij.openapi.progress.Task
import com.intellij.openapi.project.Project
import com.intellij.openapi.util.Key
import com.intellij.psi.PsiElement
import com.intellij.psi.search.GlobalSearchScope
import com.intellij.psi.search.searches.ReferencesSearch
@@ -39,6 +40,7 @@ import org.jetbrains.kotlin.idea.stubindex.KotlinSourceFilterScope
import org.jetbrains.kotlin.idea.util.application.executeWriteCommand
import org.jetbrains.kotlin.idea.util.application.runReadAction
import org.jetbrains.kotlin.psi.*
import org.jetbrains.kotlin.psi.psiUtil.collectDescendantsOfType
import org.jetbrains.kotlin.psi.psiUtil.forEachDescendantOfType
import org.jetbrains.kotlin.psi.psiUtil.getStrictParentOfType
import org.jetbrains.kotlin.resolve.BindingContext
@@ -116,84 +118,104 @@ fun UsageReplacementStrategy.replaceUsages(
) {
GuiUtils.invokeLaterIfNeeded({
project.executeWriteCommand(commandName) {
// we should delete imports later to not affect other usages
val importsToDelete = arrayListOf<KtImportDirective>()
val replacements = mutableListOf<KtElement>()
var invalidUsagesFound = false
val targetDeclaration = targetPsiElement as? KtNamedDeclaration
val usagesChildrenFirst = usages.sortChildrenFirst()
usages@ for (usage in usagesChildrenFirst) {
try {
if (!usage.isValid) {
invalidUsagesFound = true
continue
}
val usagesByFile = usages.groupBy { it.containingFile }
val usageParent = usage.parent
when (usageParent) {
is KtCallableReferenceExpression -> {
val grandParent = usageParent.parent
ConvertReferenceToLambdaIntention().applyTo(usageParent, null)
(grandParent as? KtElement)?.let {
doRefactoringInside(it, targetDeclaration?.name, targetDeclaration?.descriptor)
}
continue@usages
}
is KtCallElement -> {
val lambdaArguments = usageParent.lambdaArguments
if (lambdaArguments.isNotEmpty()) {
val grandParent = usageParent.parent
val specifySignature = SpecifyExplicitLambdaSignatureIntention()
for (lambdaArgument in lambdaArguments) {
val lambdaExpression = lambdaArgument.getLambdaExpression()
val functionDescriptor =
lambdaExpression.functionLiteral.resolveToDescriptorIfAny() as? FunctionDescriptor ?: continue
if (functionDescriptor.valueParameters.isNotEmpty()) {
specifySignature.applyTo(lambdaExpression, null)
}
}
(grandParent as? KtElement)?.let {
doRefactoringInside(it, targetDeclaration?.name, targetDeclaration?.descriptor)
}
continue@usages
}
}
val KEY = Key<Unit>("UsageReplacementStrategy.replaceUsages")
}
for ((file, usagesInFile) in usagesByFile) {
usagesInFile.forEach { it.putCopyableUserData(KEY, Unit) }
//TODO: keep the import if we don't know how to replace some of the usages
val importDirective = usage.getStrictParentOfType<KtImportDirective>()
if (importDirective != null) {
if (!importDirective.isAllUnder && importDirective.targetDescriptors().size == 1) {
importsToDelete.add(importDirective)
}
continue
}
// we should delete imports later to not affect other usages
val importsToDelete = mutableListOf<KtImportDirective>()
val replacement = createReplacer(usage)?.invoke()
if (replacement != null) {
replacements += replacement
}
}
catch (e: Throwable) {
LOG.error(e)
var usagesToProcess = usagesInFile
while (usagesToProcess.isNotEmpty()) {
if (processUsages(usagesToProcess, targetDeclaration, importsToDelete)) break
// some usages may get invalidated we need to find them in the tree
usagesToProcess = file.collectDescendantsOfType<KtSimpleNameExpression> { it.getCopyableUserData(KEY) != null }
}
file.forEachDescendantOfType<KtSimpleNameExpression> { it.putCopyableUserData(KEY, null) }
importsToDelete.forEach { it.delete() }
}
if (invalidUsagesFound && targetDeclaration != null) {
val name = targetDeclaration.name
val descriptor = targetDeclaration.descriptor
for (replacement in replacements) {
doRefactoringInside(replacement, name, descriptor)
}
}
importsToDelete.forEach { it.delete() }
postAction()
}
}, ModalityState.NON_MODAL)
}
/**
* @return false if some usages were invalidated
*/
private fun UsageReplacementStrategy.processUsages(
usages: List<KtSimpleNameExpression>,
targetDeclaration: KtNamedDeclaration?,
importsToDelete: MutableList<KtImportDirective>
): Boolean {
var invalidUsagesFound = false
for (usage in usages.sortChildrenFirst()) {
try {
if (!usage.isValid) {
invalidUsagesFound = true
continue
}
if (specialUsageProcessing(usage, targetDeclaration)) continue
//TODO: keep the import if we don't know how to replace some of the usages
val importDirective = usage.getStrictParentOfType<KtImportDirective>()
if (importDirective != null) {
if (!importDirective.isAllUnder && importDirective.targetDescriptors().size == 1) {
importsToDelete.add(importDirective)
}
continue
}
createReplacer(usage)?.invoke()
}
catch (e: Throwable) {
LOG.error(e)
}
}
return !invalidUsagesFound
}
private fun UsageReplacementStrategy.specialUsageProcessing(usage: KtSimpleNameExpression, targetDeclaration: KtNamedDeclaration?): Boolean {
val usageParent = usage.parent
when (usageParent) {
is KtCallableReferenceExpression -> {
val grandParent = usageParent.parent
ConvertReferenceToLambdaIntention().applyTo(usageParent, null)
(grandParent as? KtElement)?.let {
doRefactoringInside(it, targetDeclaration?.name, targetDeclaration?.descriptor)
}
return true
}
is KtCallElement -> {
val lambdaArguments = usageParent.lambdaArguments
if (lambdaArguments.isNotEmpty()) {
val grandParent = usageParent.parent
val specifySignature = SpecifyExplicitLambdaSignatureIntention()
for (lambdaArgument in lambdaArguments) {
val lambdaExpression = lambdaArgument.getLambdaExpression()
val functionDescriptor =
lambdaExpression.functionLiteral.resolveToDescriptorIfAny() as? FunctionDescriptor ?: continue
if (functionDescriptor.valueParameters.isNotEmpty()) {
specifySignature.applyTo(lambdaExpression, null)
}
}
(grandParent as? KtElement)?.let {
doRefactoringInside(it, targetDeclaration?.name, targetDeclaration?.descriptor)
}
return true
}
}
}
return false
}
@@ -22,7 +22,7 @@ import org.jetbrains.kotlin.idea.inspections.IntentionBasedInspection
import org.jetbrains.kotlin.psi.KtBlockStringTemplateEntry
import org.jetbrains.kotlin.psi.KtNameReferenceExpression
import org.jetbrains.kotlin.psi.KtPsiFactory
import org.jetbrains.kotlin.psi.KtStringTemplateEntryWithExpression
import org.jetbrains.kotlin.psi.KtSimpleNameStringTemplateEntry
import org.jetbrains.kotlin.psi.psiUtil.canPlaceAfterSimpleNameEntry
class RemoveCurlyBracesFromTemplateInspection : IntentionBasedInspection<KtBlockStringTemplateEntry>(RemoveCurlyBracesFromTemplateIntention::class)
@@ -37,7 +37,7 @@ class RemoveCurlyBracesFromTemplateIntention : SelfTargetingOffsetIndependentInt
applyTo(element)
}
fun applyTo(element: KtBlockStringTemplateEntry): KtStringTemplateEntryWithExpression {
fun applyTo(element: KtBlockStringTemplateEntry): KtSimpleNameStringTemplateEntry {
val name = (element.expression as KtNameReferenceExpression).getReferencedName()
val newEntry = KtPsiFactory(element).createSimpleNameStringTemplateEntry(name)
return element.replaced(newEntry)
@@ -10,7 +10,7 @@ fun newFun(): Int = 0
fun foo(): Int {
bar()
<caret>return newFun()
return <caret>newFun()
}
fun bar(): Int? = 0
@@ -0,0 +1,14 @@
class X(val v: Int) {
fun <caret>foo() {
println("foo()")
return v
}
}
fun X.f1() {
println("Value: ${foo()}")
}
fun f2(x: X) {
println("Value: ${x.foo()}")
}
@@ -0,0 +1,12 @@
class X(val v: Int) {
}
fun X.f1() {
println("foo()")
println("Value: $v")
}
fun f2(x: X) {
println("foo()")
println("Value: ${x.v}")
}
@@ -50,6 +50,12 @@ public class InlineTestGenerated extends AbstractInlineTest {
doTest(fileName);
}
@TestMetadata("InStringTemplates.kt")
public void testInStringTemplates() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/refactoring/inline/function/InStringTemplates.kt");
doTest(fileName);
}
@TestMetadata("LocalCapturing.kt")
public void testLocalCapturing() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/refactoring/inline/function/LocalCapturing.kt");