DeprecatedSymbolUsageFix preserves comments

This commit is contained in:
Valentin Kipyatkov
2015-05-26 14:14:20 +03:00
parent fefa12b0a9
commit d3403dee1a
17 changed files with 342 additions and 73 deletions
@@ -572,11 +572,11 @@ public inline fun <reified T : PsiElement> PsiElement.forEachDescendantOfType(no
})
}
public inline fun <reified T : PsiElement> PsiElement.anyDescendantOfType(noinline predicate: (T) -> Boolean): Boolean {
public inline fun <reified T : PsiElement> PsiElement.anyDescendantOfType(noinline predicate: (T) -> Boolean = { true }): Boolean {
return findDescendantOfType(predicate) != null
}
public inline fun <reified T : PsiElement> PsiElement.findDescendantOfType(noinline predicate: (T) -> Boolean): T? {
public inline fun <reified T : PsiElement> PsiElement.findDescendantOfType(noinline predicate: (T) -> Boolean = { true }): T? {
var result: T? = null
this.accept(object : PsiRecursiveElementVisitor(){
override fun visitElement(element: PsiElement) {
@@ -18,12 +18,9 @@ package org.jetbrains.kotlin.idea.core
import com.intellij.openapi.util.Key
import com.intellij.openapi.util.TextRange
import com.intellij.psi.PsiComment
import com.intellij.psi.PsiElement
import com.intellij.psi.PsiRecursiveElementVisitor
import com.intellij.psi.PsiWhiteSpace
import com.intellij.psi.util.PsiTreeUtil
import org.jetbrains.kotlin.psi.JetFile
import com.intellij.psi.*
import org.jetbrains.kotlin.lexer.JetToken
import org.jetbrains.kotlin.lexer.JetTokens
import org.jetbrains.kotlin.psi.JetPsiFactory
import org.jetbrains.kotlin.psi.psiUtil.*
import java.util.ArrayList
@@ -35,14 +32,26 @@ public class CommentSaver(originalElements: PsiChildRange) {
private val SAVED_TREE_KEY = Key<TreeElement>("SAVED_TREE")
private class TreeElement(
var parent: TreeElement? = null,
var prev: TreeElement? = null,
var next: TreeElement? = null,
var firstChild: TreeElement? = null,
var lastChild: TreeElement? = null
private abstract class TreeElement {
public companion object {
public fun create(element: PsiElement): TreeElement? {
val elementType = element.getNode().getElementType()
val treeElement = when (elementType) {
TokenType.WHITE_SPACE -> if (element.textContains('\n')) LineBreakTreeElement() else null
is JetToken -> if (element is PsiComment) CommentTreeElement.create(element) else TokenTreeElement(elementType)
else -> StandardTreeElement()
}
// treeElement?.debugText = element.getText()
return treeElement
}
}
var parent: TreeElement? = null
var prev: TreeElement? = null
var next: TreeElement? = null
var firstChild: TreeElement? = null
var lastChild: TreeElement? = null
) {
val children: Sequence<TreeElement>
get() = sequence({ firstChild }, { it.next })
@@ -69,28 +78,34 @@ public class CommentSaver(originalElements: PsiChildRange) {
// var debugText: String? = null
}
private data class CommentData(
private class StandardTreeElement() : TreeElement()
private class TokenTreeElement(val tokenType: JetToken) : TreeElement()
private class LineBreakTreeElement() : TreeElement()
private class CommentTreeElement(
val commentText: String,
val spaceBefore: String,
val spaceAfter: String
)
private fun PsiComment.buildData(): CommentData {
val spaceBefore = (prevLeaf(skipEmptyElements = true) as? PsiWhiteSpace)?.getText() ?: ""
val spaceAfter = (nextLeaf(skipEmptyElements = true) as? PsiWhiteSpace)?.getText() ?: ""
return CommentData(getText(), spaceBefore, spaceAfter)
) : TreeElement() {
public companion object {
fun create(comment: PsiComment): CommentTreeElement {
val spaceBefore = (comment.prevLeaf(skipEmptyElements = true) as? PsiWhiteSpace)?.getText() ?: ""
val spaceAfter = (comment.nextLeaf(skipEmptyElements = true) as? PsiWhiteSpace)?.getText() ?: ""
return CommentTreeElement(comment.getText(), spaceBefore, spaceAfter)
}
}
}
private val commentsToRestore = LinkedHashMap<TreeElement, CommentData>()
private val commentsToRestore = ArrayList<CommentTreeElement>()
init {
if (originalElements.any { it.anyDescendantOfTypeTemp<PsiComment>() }) {
if (originalElements.any { it.anyDescendantOfType<PsiComment>() }) {
originalElements.save(null)
for (element in originalElements) {
element.accept(object : PsiRecursiveElementVisitor(){
override fun visitComment(comment: PsiComment) {
commentsToRestore.put(comment.savedTreeElement!!, comment.buildData())
commentsToRestore.add(comment.savedTreeElement as CommentTreeElement)
}
})
}
@@ -101,12 +116,14 @@ public class CommentSaver(originalElements: PsiChildRange) {
var first: TreeElement? = null
var last: TreeElement? = null
for (child in this) {
if (!child.shouldSave) continue
assert(child.savedTreeElement == null)
val savedChild = TreeElement(parent = parentTreeElement, prev = last)
// savedChild.debugText = child.getText()
child.savedTreeElement = savedChild
val savedChild = TreeElement.create(child) ?: continue
savedChild.parent = parentTreeElement
savedChild.prev = last
if (child !is PsiWhiteSpace) { // we don't try to anchor comments to whitespaces
child.savedTreeElement = savedChild
}
last?.next = savedChild
last = savedChild
@@ -121,9 +138,6 @@ public class CommentSaver(originalElements: PsiChildRange) {
parentTreeElement?.lastChild = last
}
private val PsiElement.shouldSave: Boolean
get() = this !is PsiWhiteSpace
private var PsiElement.savedTreeElement: TreeElement?
get() = getCopyableUserData(SAVED_TREE_KEY)
set(value) = putCopyableUserData(SAVED_TREE_KEY, value)
@@ -145,7 +159,7 @@ public class CommentSaver(originalElements: PsiChildRange) {
createdElement.accept(object : PsiRecursiveElementVisitor() {
override fun visitElement(element: PsiElement) {
if (!element.shouldSave) return
if (element is PsiWhiteSpace) return
val token = original.findElementAt(element.getStartOffsetIn(createdElement) + rangeInOriginal.getStartOffset())
if (token != null) {
@@ -206,47 +220,89 @@ public class CommentSaver(originalElements: PsiChildRange) {
}
val psiFactory = JetPsiFactory(resultElements.first!!)
for ((treeElement, commentData) in commentsToRestore) {
val comment = psiFactory.createComment(commentData.commentText)
var putAbandonedCommentsAfter = resultElements.last!!
//TODO: should we restore multiple?
val putAfter: PsiElement? = treeElement.prevElements.map { toNewPsiElementMap[it]?.first() }.filterNotNull().firstOrNull()
if (putAfter != null) {
val parent = putAfter.getParent()
parent.addAfter(comment, putAfter)
if (commentData.spaceBefore.isNotEmpty()) {
parent.addAfter(psiFactory.createWhiteSpace(commentData.spaceBefore), putAfter)
for (commentTreeElement in commentsToRestore) {
val comment = psiFactory.createComment(commentTreeElement.commentText)
val anchorBefore = findAnchor(commentTreeElement, toNewPsiElementMap, before = true)
val anchorAfter = findAnchor(commentTreeElement, toNewPsiElementMap, before = false)
val anchor = chooseAnchor(anchorBefore, anchorAfter)
if (anchor != null) {
val anchorElement = findFinalAnchorElement(anchor)
val parent = anchorElement.getParent()
if (anchor.before) {
parent.addAfter(comment, anchorElement)
if (commentTreeElement.spaceBefore.isNotEmpty()) {
parent.addAfter(psiFactory.createWhiteSpace(commentTreeElement.spaceBefore), anchorElement)
}
}
continue
}
val putBefore: PsiElement? = treeElement.nextElements.map { toNewPsiElementMap[it]?.first() }.filterNotNull().firstOrNull()
if (putBefore != null) {
val parent = putBefore.getParent()
parent.addBefore(comment, putBefore)
if (commentData.spaceAfter.isNotEmpty()) {
parent.addBefore(psiFactory.createWhiteSpace(commentData.spaceAfter), putBefore)
else {
parent.addBefore(comment, anchorElement)
if (commentTreeElement.spaceAfter.isNotEmpty()) {
parent.addBefore(psiFactory.createWhiteSpace(commentTreeElement.spaceAfter), anchorElement)
}
}
continue
}
// TODO: everything deleted, don't restore anything?
else {
putAbandonedCommentsAfter = putAbandonedCommentsAfter.getParent().addAfter(comment, putAbandonedCommentsAfter)
}
}
}
//TODO
private inline fun <reified T : PsiElement> PsiElement.anyDescendantOfTypeTemp(): Boolean {
var result = false
this.accept(object : PsiRecursiveElementVisitor(){
override fun visitElement(element: PsiElement) {
if (result) return
if (element is T) {
result = true
return
}
super.visitElement(element)
private class Anchor(val element: PsiElement, val treeElementsBetween: Collection<TreeElement>, val before: Boolean)
private fun findAnchor(commentTreeElement: CommentTreeElement, toNewPsiElementMap: Map<TreeElement, Collection<PsiElement>>, before: Boolean): Anchor? {
val treeElementsBetween = ArrayList<TreeElement>()
val sequence = if (before) commentTreeElement.prevElements else commentTreeElement.nextElements
for (treeElement in sequence) {
val newPsiElements = toNewPsiElementMap[treeElement]
if (newPsiElements != null) {
var psiElement = newPsiElements.first() //TODO: should we restore multiple?
psiElement = psiElement.parents()
.dropWhile { it.getParent() !is PsiFile && (if (before) it.getNextSibling() else it.getPrevSibling()) == null }
.first()
return Anchor(psiElement, treeElementsBetween, before)
}
})
return result
if (treeElement.firstChild == null) { // we put only leafs into treeElementsBetween
treeElementsBetween.add(treeElement)
}
}
return null
}
private fun chooseAnchor(anchorBefore: Anchor?, anchorAfter: Anchor?): Anchor? {
if (anchorBefore == null) return anchorAfter
if (anchorAfter == null) return anchorBefore
val elementsBefore = anchorBefore.treeElementsBetween
val elementsAfter = anchorAfter.treeElementsBetween
val lineBreakBefore = elementsBefore.any { it is LineBreakTreeElement }
val lineBreakAfter = elementsAfter.any { it is LineBreakTreeElement }
if (lineBreakBefore && !lineBreakAfter) return anchorAfter
if (elementsBefore.isNotEmpty() && elementsAfter.isEmpty()) return anchorAfter
return anchorBefore //TODO: more analysis?
}
private fun findFinalAnchorElement(anchor: Anchor): PsiElement {
val tokensBetween = anchor.treeElementsBetween.filterIsInstance<TokenTreeElement>()
var psiElement = anchor.element
if (tokensBetween.isEmpty()) return psiElement
fun PsiElement.next(): PsiElement? {
val filter = { element: PsiElement -> element !is PsiWhiteSpace && element.getTextLength() > 0 }
return if (anchor.before) nextLeaf(filter) else prevLeaf(filter)
}
for (token in tokensBetween.reverse()) {
val next = psiElement.next() ?: break
if (next.getNode().getElementType() != token.tokenType) break
psiElement = next
}
return psiElement
}
}
@@ -333,6 +333,8 @@ fun createSpacingBuilder(settings: CodeStyleSettings): KotlinSpacingBuilder {
// if when entry has block, spacing after arrow should be set by lbrace rule
aroundInside(ARROW, WHEN_ENTRY).spaceIf(jetSettings.SPACE_AROUND_WHEN_ARROW)
after(EOL_COMMENT).lineBreakInCode()
}
}
}
@@ -29,6 +29,7 @@ import org.jetbrains.kotlin.descriptors.ValueParameterDescriptor
import org.jetbrains.kotlin.descriptors.annotations.AnnotationDescriptor
import org.jetbrains.kotlin.idea.caches.resolve.analyze
import org.jetbrains.kotlin.idea.caches.resolve.getResolutionFacade
import org.jetbrains.kotlin.idea.core.CommentSaver
import org.jetbrains.kotlin.idea.core.OptionalParametersHelper
import org.jetbrains.kotlin.idea.core.asExpression
import org.jetbrains.kotlin.idea.core.refactoring.JetNameSuggester
@@ -42,17 +43,13 @@ import org.jetbrains.kotlin.lexer.JetTokens
import org.jetbrains.kotlin.name.Name
import org.jetbrains.kotlin.name.renderName
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.getReceiverExpression
import org.jetbrains.kotlin.psi.psiUtil.isAncestor
import org.jetbrains.kotlin.psi.psiUtil.*
import org.jetbrains.kotlin.resolve.BindingContext
import org.jetbrains.kotlin.resolve.CompileTimeConstantUtils
import org.jetbrains.kotlin.resolve.DescriptorUtils
import org.jetbrains.kotlin.resolve.bindingContextUtil.getDataFlowInfo
import org.jetbrains.kotlin.resolve.bindingContextUtil.isUsedAsExpression
import org.jetbrains.kotlin.resolve.calls.callUtil.getResolvedCall
import org.jetbrains.kotlin.resolve.calls.callUtil.getValueArgumentsInParentheses
import org.jetbrains.kotlin.resolve.calls.model.*
import org.jetbrains.kotlin.resolve.constants.CompileTimeConstant
import org.jetbrains.kotlin.resolve.descriptorUtil.builtIns
@@ -62,7 +59,8 @@ import org.jetbrains.kotlin.resolve.scopes.receivers.ThisReceiver
import org.jetbrains.kotlin.types.ErrorUtils
import org.jetbrains.kotlin.types.JetType
import org.jetbrains.kotlin.utils.addIfNotNull
import java.util.*
import java.util.ArrayList
import java.util.LinkedHashSet
//TODO: replacement of class usages
//TODO: different replacements for property accessors
@@ -136,6 +134,8 @@ public abstract class DeprecatedSymbolUsageFixBase(
val qualifiedExpression = callExpression.getParent() as? JetQualifiedExpression
val expressionToBeReplaced = qualifiedExpression ?: callExpression
val commentSaver = CommentSaver(expressionToBeReplaced)
var receiver = element.getReceiverExpression()?.marked(USER_CODE_KEY)
var receiverType = if (receiver != null) bindingContext.getType(receiver) else null
@@ -225,6 +225,12 @@ public abstract class DeprecatedSymbolUsageFixBase(
result = postProcessInsertedExpression(result, wrapper.addedStatements)
val resultRange = if (wrapper.addedStatements.isEmpty())
PsiChildRange.singleElement(result)
else
PsiChildRange(wrapper.addedStatements.first(), result)
commentSaver.restoreComments(resultRange)
return result
}
@@ -0,0 +1,14 @@
// "Replace with 'newFun(p4, p3, p2, p1, p0)'" "true"
@deprecated("", ReplaceWith("newFun(p4, p3, p2, p1, p0)"))
fun oldFun(p0: Int, p1: Int, p2: Int, p3: Int, p4: Int){}
fun newFun(p4: Int, p3: Int, p2: Int, p1: Int, p0: Int){}
fun foo() {
<caret>oldFun(/* 0 */ 0,
/* 1 */ 1,
2, // 2
3, /* 4 */ 4)
}
@@ -0,0 +1,12 @@
// "Replace with 'newFun(p4, p3, p2, p1, p0)'" "true"
@deprecated("", ReplaceWith("newFun(p4, p3, p2, p1, p0)"))
fun oldFun(p0: Int, p1: Int, p2: Int, p3: Int, p4: Int){}
fun newFun(p4: Int, p3: Int, p2: Int, p1: Int, p0: Int){}
fun foo() {
newFun(/* 4 */ 4, 3, 2, // 2
/* 1 */ 1, /* 0 */ 0)
}
@@ -0,0 +1,12 @@
// "Replace with 'newFun()'" "true"
@deprecated("", ReplaceWith("newFun()"))
fun oldFun(p: Int) {
newFun()
}
fun newFun(){}
fun foo() {
<caret>oldFun(O /* use zero */)
}
@@ -0,0 +1,12 @@
// "Replace with 'newFun()'" "true"
@deprecated("", ReplaceWith("newFun()"))
fun oldFun(p: Int) {
newFun()
}
fun newFun(){}
fun foo() {
newFun()/* use zero */
}
@@ -0,0 +1,15 @@
// "Replace with 'newFun(this, p1, p2)'" "true"
interface X {
@deprecated("", ReplaceWith("newFun(this, p1, p2)"))
fun oldFun(p1: Int, p2: Int)
}
fun newFun(x: X, a: Int, b: Int){}
fun foo(x: X) {
x/*receiver*/.<caret>oldFun(
1, // pass 1
2 // pass 2
)
}
@@ -0,0 +1,14 @@
// "Replace with 'newFun(this, p1, p2)'" "true"
interface X {
@deprecated("", ReplaceWith("newFun(this, p1, p2)"))
fun oldFun(p1: Int, p2: Int)
}
fun newFun(x: X, a: Int, b: Int){}
fun foo(x: X) {
newFun(x/*receiver*/, 1, // pass 1
2 // pass 2
)
}
@@ -0,0 +1,14 @@
// "Replace with 'newFun(p)'" "true"
class X {
@deprecated("", ReplaceWith("newFun(p)"))
fun oldFun(p: Int) {
newFun(p)
}
fun newFun(p: Int){}
}
fun foo(x: X) {
x/*receiver*/.<caret>oldFun(1/*parameter*/)
}
@@ -0,0 +1,14 @@
// "Replace with 'newFun(p)'" "true"
class X {
@deprecated("", ReplaceWith("newFun(p)"))
fun oldFun(p: Int) {
newFun(p)
}
fun newFun(p: Int){}
}
fun foo(x: X) {
x/*receiver*/.<caret>newFun(1/*parameter*/)
}
@@ -0,0 +1,12 @@
// "Replace with 'newFun(p, p)'" "true"
@deprecated("", ReplaceWith("newFun(p, p)"))
fun oldFun(p: Int) {
newFun(p, p)
}
fun newFun(p1: Int, p2: Int){}
fun foo() {
<caret>oldFun(0/*use zero*/)
}
@@ -0,0 +1,12 @@
// "Replace with 'newFun(p, p)'" "true"
@deprecated("", ReplaceWith("newFun(p, p)"))
fun oldFun(p: Int) {
newFun(p, p)
}
fun newFun(p1: Int, p2: Int){}
fun foo() {
newFun(0/*use zero*/, 0)
}
@@ -0,0 +1,14 @@
// "Replace with 'newFun(p, p)'" "true"
@deprecated("", ReplaceWith("newFun(p, p)"))
fun oldFun(p: Int) {
newFun(p, p)
}
fun newFun(p1: Int, p2: Int){}
fun foo() {
<caret>oldFun(bar()/*use bar()*/)
}
fun bar(): Int = 0
@@ -0,0 +1,15 @@
// "Replace with 'newFun(p, p)'" "true"
@deprecated("", ReplaceWith("newFun(p, p)"))
fun oldFun(p: Int) {
newFun(p, p)
}
fun newFun(p1: Int, p2: Int){}
fun foo() {
val p = bar()/*use bar()*/
newFun(p, p)
}
fun bar(): Int = 0
@@ -3217,6 +3217,51 @@ public class QuickFixTestGenerated extends AbstractQuickFixTest {
}
}
@TestMetadata("idea/testData/quickfix/deprecatedSymbolUsage/keepComments")
@TestDataPath("$PROJECT_ROOT")
@RunWith(JUnit3RunnerWithInners.class)
public static class KeepComments extends AbstractQuickFixTest {
public void testAllFilesPresentInKeepComments() throws Exception {
JetTestUtils.assertAllTestsPresentByMetadata(this.getClass(), new File("idea/testData/quickfix/deprecatedSymbolUsage/keepComments"), Pattern.compile("^([\\w\\-_]+)\\.kt$"), true);
}
@TestMetadata("commentBeforeArgument.kt")
public void testCommentBeforeArgument() throws Exception {
String fileName = JetTestUtils.navigationMetadata("idea/testData/quickfix/deprecatedSymbolUsage/keepComments/commentBeforeArgument.kt");
doTest(fileName);
}
@TestMetadata("dropArgument.kt")
public void testDropArgument() throws Exception {
String fileName = JetTestUtils.navigationMetadata("idea/testData/quickfix/deprecatedSymbolUsage/keepComments/dropArgument.kt");
doTest(fileName);
}
@TestMetadata("multipleArguments.kt")
public void testMultipleArguments() throws Exception {
String fileName = JetTestUtils.navigationMetadata("idea/testData/quickfix/deprecatedSymbolUsage/keepComments/multipleArguments.kt");
doTest(fileName);
}
@TestMetadata("simple.kt")
public void testSimple() throws Exception {
String fileName = JetTestUtils.navigationMetadata("idea/testData/quickfix/deprecatedSymbolUsage/keepComments/simple.kt");
doTest(fileName);
}
@TestMetadata("usedTwice.kt")
public void testUsedTwice() throws Exception {
String fileName = JetTestUtils.navigationMetadata("idea/testData/quickfix/deprecatedSymbolUsage/keepComments/usedTwice.kt");
doTest(fileName);
}
@TestMetadata("variableForArgument.kt")
public void testVariableForArgument() throws Exception {
String fileName = JetTestUtils.navigationMetadata("idea/testData/quickfix/deprecatedSymbolUsage/keepComments/variableForArgument.kt");
doTest(fileName);
}
}
@TestMetadata("idea/testData/quickfix/deprecatedSymbolUsage/optionalParameters")
@TestDataPath("$PROJECT_ROOT")
@RunWith(JUnit3RunnerWithInners.class)