Workaround KT-14531, Partial fix for KT-13600, KT-11620 J2K: Invalid conversion context on plain text pasting

For detailed info see KT-14531
This commit is contained in:
Simon Ogorodnik
2016-10-26 21:34:03 +03:00
parent f8b7ce1011
commit 0462a3d3b6
17 changed files with 436 additions and 78 deletions
@@ -86,7 +86,7 @@ class ConvertJavaCopyPasteProcessor : CopyPastePostProcessor<TextBlockTransferab
fun doConversion(): Result {
val dataForConversion = DataForConversion.prepare(data, project)
val result = convertCopiedCodeToKotlin(dataForConversion.elementsAndTexts, project)
val result = dataForConversion.elementsAndTexts.convertCodeToKotlin(project)
val referenceData = buildReferenceData(result.text, result.parseContext, dataForConversion.importsAndPackage, targetFile)
val text = if (result.textChanged) result.text else null
return Result(text, referenceData, result.importsToAdd)
@@ -198,14 +198,14 @@ internal class ConversionResult(
val textChanged: Boolean
)
internal fun convertCopiedCodeToKotlin(elementsAndTexts: Collection<Any>, project: Project): ConversionResult {
internal fun ElementAndTextList.convertCodeToKotlin(project: Project): ConversionResult {
val converter = JavaToKotlinConverter(
project,
ConverterSettings.defaultSettings,
IdeaJavaToKotlinServices
)
val inputElements = elementsAndTexts.filterIsInstance<PsiElement>()
val inputElements = this.toList().filterIsInstance<PsiElement>()
val results = converter.elementsToKotlin(inputElements).results
val importsToAdd = LinkedHashSet<FqName>()
@@ -213,9 +213,9 @@ internal fun convertCopiedCodeToKotlin(elementsAndTexts: Collection<Any>, projec
val convertedCodeBuilder = StringBuilder()
val originalCodeBuilder = StringBuilder()
var parseContext: ParseContext? = null
for (o in elementsAndTexts) {
if (o is PsiElement) {
val originalText = o.text
this.process(object : ElementsAndTextsProcessor {
override fun processElement(element: PsiElement) {
val originalText = element.text
originalCodeBuilder.append(originalText)
val result = results[resultIndex++]
@@ -230,11 +230,12 @@ internal fun convertCopiedCodeToKotlin(elementsAndTexts: Collection<Any>, projec
convertedCodeBuilder.append(originalText)
}
}
else {
originalCodeBuilder.append(o)
convertedCodeBuilder.append(o as String)
override fun processText(string: String) {
originalCodeBuilder.append(string)
convertedCodeBuilder.append(string)
}
}
})
val convertedCode = convertedCodeBuilder.toString()
val originalCode = originalCodeBuilder.toString()
@@ -27,22 +27,23 @@ import com.intellij.openapi.project.DumbService
import com.intellij.openapi.project.Project
import com.intellij.openapi.util.Ref
import com.intellij.openapi.util.TextRange
import com.intellij.psi.*
import com.intellij.psi.PsiDocumentManager
import com.intellij.psi.PsiErrorElement
import com.intellij.psi.PsiFile
import com.intellij.psi.PsiFileFactory
import com.intellij.util.LocalTimeCounter
import org.jetbrains.annotations.TestOnly
import org.jetbrains.kotlin.asJava.toLightClass
import org.jetbrains.kotlin.idea.KotlinFileType
import org.jetbrains.kotlin.idea.codeInsight.referenceExpression
import org.jetbrains.kotlin.idea.editor.KotlinEditorOptions
import org.jetbrains.kotlin.idea.j2k.J2kPostProcessor
import org.jetbrains.kotlin.idea.references.mainReference
import org.jetbrains.kotlin.idea.util.application.runWriteAction
import org.jetbrains.kotlin.j2k.AfterConversionPass
import org.jetbrains.kotlin.lexer.KtTokens
import org.jetbrains.kotlin.psi.*
import org.jetbrains.kotlin.psi.psiUtil.anyDescendantOfType
import org.jetbrains.kotlin.psi.psiUtil.endOffset
import org.jetbrains.kotlin.psi.psiUtil.getParentOfType
import org.jetbrains.kotlin.resolve.ImportPath
import org.jetbrains.kotlin.utils.addToStdlib.check
import org.jetbrains.kotlin.utils.addToStdlib.ifNotEmpty
import java.awt.datatransfer.DataFlavor
@@ -103,10 +104,21 @@ class ConvertTextJavaCopyPasteProcessor : CopyPastePostProcessor<TextBlockTransf
if (!confirmConvertJavaOnPaste(project, isPlainText = true)) return
val copiedJavaCode = prepareCopiedJavaCodeByContext(text, conversionContext, pasteTarget)
val dataForConversion = DataForConversion.prepare(copiedJavaCode, project)
val convertedText = copiedJavaCode.convertCodeToKotlin(project).text
val additionalImports = dataForConversion.tryResolveImports(targetFile)
var convertedImportsText = additionalImports.convertCodeToKotlin(project).text
val convertedText = dataForConversion.convertCodeToKotlin(project).text
runWriteAction {
val importsInsertOffset = targetFile.importList?.endOffset ?: 0
if (targetFile.importDirectives.isEmpty() && importsInsertOffset > 0)
convertedImportsText = "\n" + convertedImportsText
if (convertedImportsText.isNotBlank())
editor.document.insertString(importsInsertOffset, convertedImportsText)
val startOffset = bounds.startOffset
editor.document.replaceString(startOffset, bounds.endOffset, convertedText)
@@ -116,16 +128,15 @@ class ConvertTextJavaCopyPasteProcessor : CopyPastePostProcessor<TextBlockTransf
val newBounds = TextRange(startOffset, startOffset + convertedText.length)
psiDocumentManager.commitAllDocuments()
AfterConversionPass(project, J2kPostProcessor(formatCode = true)).run(targetFile, newBounds)
conversionPerformed = true
}
}
private fun CopiedJavaCode.convertCodeToKotlin(project: Project): ConversionResult {
val dataForConversion = DataForConversion.prepare(this, project)
val conversionResult = convertCopiedCodeToKotlin(dataForConversion.elementsAndTexts, project)
return conversionResult
private fun DataForConversion.convertCodeToKotlin(project: Project): ConversionResult {
return elementsAndTexts.convertCodeToKotlin(project)
}
private val KtElement.pasteContext: KotlinContext
@@ -211,7 +222,14 @@ class ConvertTextJavaCopyPasteProcessor : CopyPastePostProcessor<TextBlockTransf
}
private fun parseAsFile(text: String, fileType: LanguageFileType, project: Project): PsiFile {
return PsiFileFactory.getInstance(project).createFileFromText("Dummy", fileType, text, LocalTimeCounter.currentTime(), true)
return PsiFileFactory.getInstance(project).createFileFromText("Dummy.java", fileType, text, LocalTimeCounter.currentTime(), true)
}
private fun DataForConversion.tryResolveImports(targetFile: KtFile): ElementAndTextList {
val importResolver = PlainTextPasteImportResolver(this, targetFile)
importResolver.addImportsFromTargetFile()
importResolver.tryResolveReferences()
return ElementAndTextList(importResolver.addedImports.flatMap { listOf("\n", it) } + "\n\n") //TODO Non-manual formatting for import list
}
private fun prepareCopiedJavaCodeByContext(text: String, context: JavaContext, target: KtElement): CopiedJavaCode {
@@ -225,53 +243,6 @@ class ConvertTextJavaCopyPasteProcessor : CopyPastePostProcessor<TextBlockTransf
append(";\n")
}
}
fun appendImport(importPath: ImportPath) {
append("import ")
append(importPath)
append(";\n")
}
fun appendStaticImport(importPath: ImportPath) {
append("import static ")
append(importPath)
append(";\n")
}
targetFile.importDirectives.forEach {
val importPath = it.importPath
val importedReference = it.importedReference
if (importPath != null && !importPath.hasAlias() && importedReference is KtDotQualifiedExpression) {
val receiver = importedReference
.receiverExpression
.referenceExpression()
?.mainReference
?.resolve()
val selector = importedReference
.selectorExpression
?.referenceExpression()
?.mainReference
?.resolve()
val isPackageReceiver = receiver is PsiPackage
val isClassReceiver = receiver is PsiClass
val isClassSelector = selector is PsiClass
if (importPath.isAllUnder) {
if (isClassReceiver)
appendStaticImport(importPath)
else if (isPackageReceiver)
appendImport(importPath)
}
else {
if (isPackageReceiver && isClassSelector)
appendImport(importPath)
else if (isClassReceiver)
appendStaticImport(importPath)
}
}
}
}
val classDef = when (context) {
@@ -300,11 +271,12 @@ class ConvertTextJavaCopyPasteProcessor : CopyPastePostProcessor<TextBlockTransf
JavaContext.EXPRESSION -> createCopiedJavaCode(prefix, "$classDef {\nObject field = $\n}", text)
}
return copiedJavaCode
}
private fun createCopiedJavaCode(prefix: String, templateWithoutPrefix: String, text: String): CopiedJavaCode {
val template = "$prefix\n$templateWithoutPrefix"
val template = "$prefix$templateWithoutPrefix"
val index = template.indexOf("$")
assert(index >= 0)
val fileText = template.substring(0, index) + text + template.substring(index + 1)
@@ -29,8 +29,9 @@ import org.jetbrains.kotlin.psi.psiUtil.siblings
import java.util.*
data class DataForConversion private constructor(
val elementsAndTexts: Collection<Any> /* list consisting of PsiElement's to convert and plain String's */,
val importsAndPackage: String
val elementsAndTexts: ElementAndTextList /* list consisting of PsiElement's to convert and plain String's */,
val importsAndPackage: String,
val file: PsiJavaFile
) {
companion object {
fun prepare(copiedCode: CopiedJavaCode, project: Project): DataForConversion {
@@ -49,12 +50,12 @@ data class DataForConversion private constructor(
file = PsiFileFactory.getInstance(project).createFileFromText(JavaLanguage.INSTANCE, newFileText) as PsiJavaFile
}
val elementsAndTexts = ArrayList<Any>()
val elementsAndTexts = ElementAndTextList()
for (i in startOffsets.indices) {
elementsAndTexts.collectElementsToConvert(file, fileText, TextRange(startOffsets[i], endOffsets[i]))
}
return DataForConversion(elementsAndTexts, importsAndPackage)
return DataForConversion(elementsAndTexts, importsAndPackage, file)
}
private fun clipTextIfNeeded(file: PsiJavaFile, fileText: String, startOffsets: IntArray, endOffsets: IntArray): String? {
@@ -207,7 +208,7 @@ data class DataForConversion private constructor(
return clipTo
}
private fun MutableList<Any>.collectElementsToConvert(
private fun ElementAndTextList.collectElementsToConvert(
file: PsiJavaFile,
fileText: String,
range: TextRange
@@ -218,8 +219,11 @@ data class DataForConversion private constructor(
}
else {
add(fileText.substring(range.start, elements.first().range.start))
elements.flatMapTo(this) {
if (shouldExpandToChildren(it)) it.allChildren.toList() else listOf(it)
elements.forEach {
if (shouldExpandToChildren(it))
this += it.allChildren.toList()
else
this += it
}
add(fileText.substring(elements.last().range.end, range.end))
}
@@ -0,0 +1,68 @@
/*
* Copyright 2010-2016 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jetbrains.kotlin.idea.conversion.copy
import com.intellij.psi.PsiElement
import java.util.*
class ElementAndTextList() {
private val elementsAndTexts = ArrayList<Any>()
constructor(elements: List<Any>) : this() {
elementsAndTexts.addAll(elements.filter { it is PsiElement || it is String })
}
fun add(a: String) = elementsAndTexts.add(a)
fun add(a: PsiElement) = elementsAndTexts.add(a)
operator fun plusAssign(other: String) = plusAssign(other as Any)
operator fun plusAssign(other: PsiElement) = plusAssign(other as Any)
private fun plusAssign(a: Any) {
elementsAndTexts.add(a)
}
operator fun plusAssign(other: Collection<PsiElement>): Unit {
elementsAndTexts.addAll(other)
}
operator fun plus(other: ElementAndTextList): ElementAndTextList {
val newList = ElementAndTextList()
newList.elementsAndTexts.addAll(this.elementsAndTexts)
newList.elementsAndTexts.addAll(other.elementsAndTexts)
return newList
}
fun toList(): List<Any> = elementsAndTexts.toList()
fun process(processor: ElementsAndTextsProcessor) {
elementsAndTexts.forEach {
when (it) {
is PsiElement -> processor.processElement(it)
is String -> processor.processText(it)
}
}
}
}
interface ElementsAndTextsProcessor {
fun processElement(element: PsiElement)
fun processText(string: String)
}
@@ -0,0 +1,172 @@
/*
* Copyright 2010-2016 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jetbrains.kotlin.idea.conversion.copy
import com.intellij.psi.*
import com.intellij.psi.search.PsiShortNamesCache
import com.intellij.psi.util.PsiTreeUtil
import org.jetbrains.kotlin.descriptors.DeclarationDescriptorWithVisibility
import org.jetbrains.kotlin.idea.caches.resolve.analyzeFully
import org.jetbrains.kotlin.idea.caches.resolve.getJavaMemberDescriptor
import org.jetbrains.kotlin.idea.caches.resolve.getResolutionFacade
import org.jetbrains.kotlin.idea.caches.resolve.resolveToDescriptor
import org.jetbrains.kotlin.idea.codeInsight.referenceExpression
import org.jetbrains.kotlin.idea.core.isVisible
import org.jetbrains.kotlin.idea.imports.canBeReferencedViaImport
import org.jetbrains.kotlin.idea.references.mainReference
import org.jetbrains.kotlin.idea.util.application.runWriteAction
import org.jetbrains.kotlin.platform.JavaToKotlinClassMap
import org.jetbrains.kotlin.psi.KtDotQualifiedExpression
import org.jetbrains.kotlin.psi.KtFile
import org.jetbrains.kotlin.psi.KtImportDirective
import java.util.*
class PlainTextPasteImportResolver(dataForConversion: DataForConversion, val targetFile: KtFile) {
private val file = dataForConversion.file
private val project = targetFile.project
private val importList = file.importList!!
private val psiElementFactory = PsiElementFactory.SERVICE.getInstance(project)
private val bindingContext by lazy { targetFile.analyzeFully() }
private val resolutionFacade = targetFile.getResolutionFacade()
private val shortNameCache = PsiShortNamesCache.getInstance(project)
private val scope = file.resolveScope
val failedToResolveReferenceNames = HashSet<String>()
var ambiguityInResolution = false
var couldNotResolve = false
val addedImports = ArrayList<PsiImportStatementBase>()
private fun canBeImported(descriptor: DeclarationDescriptorWithVisibility?): Boolean {
return descriptor != null
&& descriptor.canBeReferencedViaImport()
&& descriptor.isVisible(targetFile, null, bindingContext, resolutionFacade)
}
private fun addImport(importStatement: PsiImportStatementBase, shouldAddToTarget: Boolean = false) {
importList.add(importStatement)
if (shouldAddToTarget)
addedImports.add(importStatement)
}
fun addImportsFromTargetFile() {
fun tryConvertKotlinImport(importDirective: KtImportDirective) {
val importPath = importDirective.importPath
val importedReference = importDirective.importedReference
if (importPath != null && !importPath.hasAlias() && importedReference is KtDotQualifiedExpression) {
val receiver = importedReference
.receiverExpression
.referenceExpression()
?.mainReference
?.resolve()
val selector = importedReference
.selectorExpression
?.referenceExpression()
?.mainReference
?.resolve()
val isPackageReceiver = receiver is PsiPackage
val isClassReceiver = receiver is PsiClass
val isClassSelector = selector is PsiClass
if (importPath.isAllUnder) {
if (isClassReceiver)
psiElementFactory.createImportStaticStatement(receiver as PsiClass, "*")
else if (isPackageReceiver)
psiElementFactory.createImportStatementOnDemand((receiver as PsiPackage).qualifiedName)
}
else {
if (isPackageReceiver && isClassSelector)
psiElementFactory.createImportStatement(selector as PsiClass)
else if (isClassReceiver)
psiElementFactory.createImportStaticStatement(receiver as PsiClass, importPath.importedName!!.asString())
}
}
}
runWriteAction {
targetFile.importDirectives.forEach(::tryConvertKotlinImport)
}
}
fun tryResolveReferences() {
val elementsWithUnresolvedRef = PsiTreeUtil.collectElements(file) {
it.reference != null
&& it.reference is PsiQualifiedReference
&& it.reference?.resolve() == null
}
fun tryResolveReference(reference: PsiQualifiedReference): Boolean {
if (reference.resolve() != null) return true
val referenceName = reference.referenceName!!
if (referenceName in failedToResolveReferenceNames) return false
val classes = shortNameCache.getClassesByName(referenceName, scope)
.map { it to it.resolveToDescriptor(resolutionFacade) }
.filter { canBeImported(it.second) }
classes.find { (psiClass, descriptor) -> JavaToKotlinClassMap.INSTANCE.mapPlatformClass(descriptor!!).isNotEmpty() }
?.let { (psiClass, descriptor) -> addImport(psiElementFactory.createImportStatement(psiClass)) }
if (reference.resolve() != null) return true
classes.singleOrNull()?.let { (psiClass, descriptor) ->
addImport(psiElementFactory.createImportStatement(psiClass), true)
}
if (reference.resolve() != null) return true
else {
if (classes.isNotEmpty()) {
ambiguityInResolution = true
return false
}
}
val members = (shortNameCache.getMethodsByName(referenceName, scope).asList() +
shortNameCache.getFieldsByName(referenceName, scope).asList())
.map { it as PsiMember }
.map { it to it.getJavaMemberDescriptor(resolutionFacade) as? DeclarationDescriptorWithVisibility }
.filter { canBeImported(it.second) }
members.singleOrNull()?.let { (psiMember, descriptor) ->
addImport(psiElementFactory.createImportStaticStatement(psiMember.containingClass!!, psiMember.name!!), true)
}
if (reference.resolve() != null) return false
else {
if (members.isNotEmpty()) {
ambiguityInResolution = true
}
else {
couldNotResolve = true
}
}
return false
}
runWriteAction {
elementsWithUnresolvedRef.forEach {
val reference = it.reference as PsiQualifiedReference
if (!tryResolveReference(reference)) failedToResolveReferenceNames += reference.referenceName!!
}
}
}
}
@@ -1,3 +1,5 @@
import java.util.ArrayList
fun foo() {
bar(ArrayList<String>())
}
}
@@ -1 +1,3 @@
fun foo() = ArrayList<String>()
import java.util.ArrayList
fun foo() = ArrayList<String>()
@@ -0,0 +1,34 @@
package test;
public class ToBeImportedJava {
public static final String TO_BE_IMPORTED_CONST = "!!!";
public static void staticMethod() {
}
}
public interface IAmbiguousJava {
}
public interface AmbiguousJava {
}
public interface IAmbiguous {
}
public interface Ambiguous {
}
public class Z {
public interface IAmbiguousJava {
}
public interface AmbiguousJava {
}
}
@@ -0,0 +1,32 @@
package test
class ToBeImportedKotlin {
}
interface IAmbiguous {
}
interface Ambiguous {
}
interface IAmbiguousKotlin {
}
interface AmbiguousKotlin {
}
class Z {
interface IAmbiguousKotlin {
}
interface AmbiguousKotlin {
}
}
@@ -0,0 +1,31 @@
import java.util.ArrayList
import test.ToBeImportedJava
import test.ToBeImportedKotlin
import java.util.HashMap
import test.ToBeImportedJava.TO_BE_IMPORTED_CONST
import test.ToBeImportedJava.staticMethod
class Target {
var listOfPlatformType: List<String> = ArrayList()
var unresolved: UnresolvedInterface<UnresolvedGeneric> = UnresolvedImplementation() // Should not add import
var hashMapOfNotImported: Map<ToBeImportedJava, ToBeImportedKotlin> = HashMap()
fun acceptKotlinClass(tbi: ToBeImportedKotlin) {
}
fun acceptJavaClass(tbi: ToBeImportedJava) {
}
var ambiguousKotlin: IAmbiguousKotlin = AmbiguousKotlin() // Should not add import in case of 2 declarations in Kotlin
var ambiguous: IAmbiguous = Ambiguous() // Should not add import in case of ambiguous declarations in Kotlin and in Java
var ambiguousJava: IAmbiguousJava = AmbiguousJava() // Should not add import in case of 2 declarations in Java
fun workWithStatics() {
val a = TO_BE_IMPORTED_CONST
staticMethod()
}
}
@@ -0,0 +1,3 @@
class Target {
<caret>
}
@@ -0,0 +1,22 @@
public List<String> listOfPlatformType = new ArrayList<String>();
UnresolvedInterface<UnresolvedGeneric> unresolved = new UnresolvedImplementation<>(); // Should not add import
Map<ToBeImportedJava, ToBeImportedKotlin> hashMapOfNotImported = new HashMap<>();
void acceptKotlinClass(ToBeImportedKotlin tbi) {
}
void acceptJavaClass(ToBeImportedJava tbi) {
}
IAmbiguousKotlin ambiguousKotlin = new AmbiguousKotlin(); // Should not add import in case of 2 declarations in Kotlin
IAmbiguous ambiguous = new Ambiguous(); // Should not add import in case of ambiguous declarations in Kotlin and in Java
IAmbiguousJava ambiguousJava = new AmbiguousJava(); // Should not add import in case of 2 declarations in Java
void workWithStatics() {
String a = TO_BE_IMPORTED_CONST;
staticMethod();
}
@@ -1,3 +1,5 @@
import java.util.ArrayList
fun foo() {
bar(/*comment*/ArrayList<String>())
}
}
@@ -1,3 +1,5 @@
import java.util.ArrayList
class A {
fun foo() {
val list = ArrayList<String>()
@@ -1,5 +1,8 @@
package to
import java.util.ArrayList
fun foo() {
val list = ArrayList<String>()
list.add(1)
@@ -1,3 +1,5 @@
import java.util.ArrayList
fun foo() {
val list = ArrayList<String>()
list.add(1)
@@ -47,6 +47,12 @@ public class TextJavaToKotlinCopyPasteConversionTestGenerated extends AbstractTe
doTest(fileName);
}
@TestMetadata("ImportResolve.txt")
public void testImportResolve() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/copyPaste/plainTextConversion/ImportResolve.txt");
doTest(fileName);
}
@TestMetadata("InsideIdentifier.txt")
public void testInsideIdentifier() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/copyPaste/plainTextConversion/InsideIdentifier.txt");