KT-24515 Intention to add an exception under the cursor to @Throws annotations

This commit is contained in:
Toshiaki Kameyama
2018-11-05 20:25:00 +09:00
committed by Yan Zhulanow
parent 3918ec71be
commit a621171d9b
48 changed files with 623 additions and 0 deletions
@@ -0,0 +1,6 @@
class MyException : Exception()
<spot>@Throws(MyException::class)</spot>
fun test() {
throw MyException()
}
@@ -0,0 +1,5 @@
class MyException : Exception()
fun test() {
throw MyException()
}
@@ -0,0 +1,5 @@
<html>
<body>
This intention adds a <b>@Throws</b> annotation for an exception under the caret.
</body>
</html>
+5
View File
@@ -1665,6 +1665,11 @@
<category>Kotlin</category>
</intentionAction>
<intentionAction>
<className>org.jetbrains.kotlin.idea.intentions.AddThrowsAnnotationIntention</className>
<category>Kotlin</category>
</intentionAction>
<lang.inspectionSuppressor language="kotlin" implementationClass="org.jetbrains.kotlin.idea.inspections.KotlinInspectionSuppressor"/>
<localInspection implementationClass="org.jetbrains.kotlin.idea.intentions.ObjectLiteralToLambdaInspection"
@@ -0,0 +1,138 @@
/*
* Copyright 2010-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license
* that can be found in the license/LICENSE.txt file.
*/
package org.jetbrains.kotlin.idea.intentions
import com.intellij.openapi.editor.Editor
import org.jetbrains.kotlin.descriptors.DeclarationDescriptorWithVisibility
import org.jetbrains.kotlin.descriptors.Visibilities
import org.jetbrains.kotlin.idea.caches.resolve.analyze
import org.jetbrains.kotlin.idea.caches.resolve.resolveToCall
import org.jetbrains.kotlin.idea.core.ShortenReferences
import org.jetbrains.kotlin.idea.core.replaced
import org.jetbrains.kotlin.idea.project.platform
import org.jetbrains.kotlin.idea.util.addAnnotation
import org.jetbrains.kotlin.name.FqName
import org.jetbrains.kotlin.psi.*
import org.jetbrains.kotlin.psi.psiUtil.getParentOfTypesAndPredicate
import org.jetbrains.kotlin.resolve.BindingContext
import org.jetbrains.kotlin.resolve.calls.callUtil.getType
import org.jetbrains.kotlin.resolve.descriptorUtil.fqNameSafe
import org.jetbrains.kotlin.resolve.jvm.platform.JvmPlatform
import org.jetbrains.kotlin.resolve.lazy.BodyResolveMode
import org.jetbrains.kotlin.types.KotlinType
import org.jetbrains.kotlin.types.getAbbreviatedType
class AddThrowsAnnotationIntention : SelfTargetingIntention<KtThrowExpression>(
KtThrowExpression::class.java, "Add '@Throws' annotation"
) {
override fun isApplicableTo(element: KtThrowExpression, caretOffset: Int): Boolean {
if (element.platform != JvmPlatform) return false
val containingDeclaration = element.getContainingDeclaration() ?: return false
val type = element.thrownExpression?.resolveToCall()?.resultingDescriptor?.returnType ?: return false
if ((type.constructor.declarationDescriptor as? DeclarationDescriptorWithVisibility)?.visibility == Visibilities.LOCAL) return false
val context = element.analyze(BodyResolveMode.PARTIAL)
val annotationEntry = containingDeclaration.findThrowsAnnotation(context) ?: return true
val valueArguments = annotationEntry.valueArguments
if (valueArguments.isEmpty()) return true
val argumentExpression = valueArguments.firstOrNull()?.getArgumentExpression()
if (argumentExpression is KtCallExpression
&& argumentExpression.calleeExpression?.getCallableDescriptor()?.fqNameSafe != FqName("kotlin.arrayOf")
) return false
return valueArguments.none { it.hasType(type, context) }
}
override fun applyTo(element: KtThrowExpression, editor: Editor?) {
val containingDeclaration = element.getContainingDeclaration() ?: return
val type = element.thrownExpression?.resolveToCall()?.resultingDescriptor?.returnType ?: return
val annotationArgumentText = if (type.getAbbreviatedType() != null)
"$type::class"
else
type.constructor.declarationDescriptor?.fqNameSafe?.let { "$it::class" } ?: return
val context = element.analyze(BodyResolveMode.PARTIAL)
val annotationEntry = containingDeclaration.findThrowsAnnotation(context)
if (annotationEntry == null || annotationEntry.valueArguments.isEmpty()) {
annotationEntry?.delete()
val whiteSpaceText = if (containingDeclaration is KtPropertyAccessor) " " else "\n"
containingDeclaration.addAnnotation(throwsAnnotationFqName, annotationArgumentText, whiteSpaceText)
} else {
val factory = KtPsiFactory(element)
val argument = annotationEntry.valueArguments.firstOrNull()
val expression = argument?.getArgumentExpression()
val added = when {
argument?.getArgumentName() == null ->
annotationEntry.valueArgumentList?.addArgument(factory.createArgument(annotationArgumentText))
expression is KtCallExpression ->
expression.valueArgumentList?.addArgument(factory.createArgument(annotationArgumentText))
expression is KtClassLiteralExpression -> {
expression.replaced(
factory.createCollectionLiteral(listOf(expression), annotationArgumentText)
).getInnerExpressions().lastOrNull()
}
expression is KtCollectionLiteralExpression -> {
expression.replaced(
factory.createCollectionLiteral(expression.getInnerExpressions(), annotationArgumentText)
).getInnerExpressions().lastOrNull()
}
else -> null
}
if (added != null) ShortenReferences.DEFAULT.process(added)
}
}
}
private val throwsAnnotationFqName = FqName("kotlin.jvm.Throws")
private fun KtThrowExpression.getContainingDeclaration(): KtDeclaration? {
val parent = getParentOfTypesAndPredicate(
true,
KtNamedFunction::class.java,
KtSecondaryConstructor::class.java,
KtPropertyAccessor::class.java,
KtClassInitializer::class.java,
KtLambdaExpression::class.java
) { true }
if (parent is KtClassInitializer || parent is KtLambdaExpression) return null
return parent as? KtDeclaration
}
private fun KtDeclaration.findThrowsAnnotation(context: BindingContext): KtAnnotationEntry? {
val annotationEntries = this.annotationEntries + (parent as? KtProperty)?.annotationEntries.orEmpty()
return annotationEntries.find {
val typeReference = it.typeReference ?: return@find false
context[BindingContext.TYPE, typeReference]?.constructor?.declarationDescriptor?.fqNameSafe == throwsAnnotationFqName
}
}
private fun ValueArgument.hasType(type: KotlinType, context: BindingContext): Boolean {
val argumentExpression = getArgumentExpression()
val expressions = when (argumentExpression) {
is KtClassLiteralExpression -> listOf(argumentExpression)
is KtCollectionLiteralExpression -> argumentExpression.getInnerExpressions().filterIsInstance(KtClassLiteralExpression::class.java)
is KtCallExpression -> argumentExpression.valueArguments.mapNotNull { it.getArgumentExpression() as? KtClassLiteralExpression }
else -> emptyList()
}
return expressions.any { it.getType(context)?.arguments?.firstOrNull()?.type == type }
}
private fun KtPsiFactory.createCollectionLiteral(expressions: List<KtExpression>, lastExpression: String): KtCollectionLiteralExpression {
return buildExpression {
appendFixedText("[")
expressions.forEach {
appendExpression(it)
appendFixedText(", ")
}
appendFixedText(lastExpression)
appendFixedText("]")
} as KtCollectionLiteralExpression
}
@@ -0,0 +1 @@
org.jetbrains.kotlin.idea.intentions.AddThrowsAnnotationIntention
@@ -0,0 +1,5 @@
fun test() {
<caret>throw java.io.IOException()
}
// RUNTIME_WITH_FULL_JDK
@@ -0,0 +1,8 @@
import java.io.IOException
@Throws(IOException::class)
fun test() {
throw java.io.IOException()
}
// RUNTIME_WITH_FULL_JDK
@@ -0,0 +1,8 @@
class FooException : Exception()
@Throws(FooException::class)
fun test() {
<caret>throw java.io.IOException()
}
// RUNTIME_WITH_FULL_JDK
@@ -0,0 +1,10 @@
import java.io.IOException
class FooException : Exception()
@Throws(FooException::class, IOException::class)
fun test() {
throw java.io.IOException()
}
// RUNTIME_WITH_FULL_JDK
@@ -0,0 +1,8 @@
// WITH_RUNTIME
class Test {
fun a() {
<caret>throw myError()
}
}
fun myError() = RuntimeException()
@@ -0,0 +1,9 @@
// WITH_RUNTIME
class Test {
@Throws(RuntimeException::class)
fun a() {
throw myError()
}
}
fun myError() = RuntimeException()
@@ -0,0 +1,9 @@
// WITH_RUNTIME
class FooException : Exception()
class BarException : Exception()
@Throws(BarException::class)
fun test() {
<caret>throw FooException()
}
@@ -0,0 +1,9 @@
// WITH_RUNTIME
class FooException : Exception()
class BarException : Exception()
@Throws(BarException::class, FooException::class)
fun test() {
throw FooException()
}
@@ -0,0 +1,10 @@
// WITH_RUNTIME
// DISABLE-ERRORS
class FooException : Exception()
class BarException : Exception()
@Throws(exceptionClasses = BarException::class)
fun test() {
<caret>throw FooException()
}
@@ -0,0 +1,10 @@
// WITH_RUNTIME
// DISABLE-ERRORS
class FooException : Exception()
class BarException : Exception()
@Throws(exceptionClasses = [BarException::class, FooException::class])
fun test() {
throw FooException()
}
@@ -0,0 +1,9 @@
// WITH_RUNTIME
class FooException : Exception()
class BarException : Exception()
@Throws(exceptionClasses = [BarException::class])
fun test() {
<caret>throw FooException()
}
@@ -0,0 +1,9 @@
// WITH_RUNTIME
class FooException : Exception()
class BarException : Exception()
@Throws(exceptionClasses = [BarException::class, FooException::class])
fun test() {
throw FooException()
}
@@ -0,0 +1,9 @@
// WITH_RUNTIME
class FooException : Exception()
class BarException : Exception()
@Throws(exceptionClasses = arrayOf(BarException::class))
fun test() {
<caret>throw FooException()
}
@@ -0,0 +1,9 @@
// WITH_RUNTIME
class FooException : Exception()
class BarException : Exception()
@Throws(exceptionClasses = arrayOf(BarException::class, FooException::class))
fun test() {
throw FooException()
}
@@ -0,0 +1,12 @@
// DISABLE-ERRORS
// WITH_RUNTIME
// IS_APPLICABLE: false
class FooException : Exception()
class BarException : Exception()
@Throws(exceptionClasses = listOf(BarException::class))
fun test() {
<caret>throw FooException()
}
@@ -0,0 +1,7 @@
// WITH_RUNTIME
class FooException : Exception()
@Throws()
fun test() {
<caret>throw FooException()
}
@@ -0,0 +1,7 @@
// WITH_RUNTIME
class FooException : Exception()
@Throws(FooException::class)
fun test() {
throw FooException()
}
@@ -0,0 +1,7 @@
// WITH_RUNTIME
class FooException : Exception()
@Throws
fun test() {
throw FooException()<caret>
}
@@ -0,0 +1,7 @@
// WITH_RUNTIME
class FooException : Exception()
@Throws(FooException::class)
fun test() {
throw FooException()
}
@@ -0,0 +1,9 @@
// WITH_RUNTIME
// IS_APPLICABLE: false
class FooException : Exception()
@Throws(FooException::class)
fun test() {
<caret>throw FooException()
}
@@ -0,0 +1,10 @@
// WITH_RUNTIME
// DISABLE-ERRORS
// IS_APPLICABLE: false
class FooException : Exception()
@Throws(exceptionClasses = FooException::class)
fun test() {
<caret>throw FooException()
}
@@ -0,0 +1,9 @@
// WITH_RUNTIME
// IS_APPLICABLE: false
class FooException : Exception()
@Throws(exceptionClasses = [FooException::class])
fun test() {
<caret>throw FooException()
}
@@ -0,0 +1,9 @@
// WITH_RUNTIME
// IS_APPLICABLE: false
class FooException : Exception()
@Throws(exceptionClasses = arrayOf(FooException::class))
fun test() {
<caret>throw FooException()
}
+5
View File
@@ -0,0 +1,5 @@
// WITH_RUNTIME
fun a(b: Boolean) {
<caret>throw if (b) RuntimeException() else Exception()
}
@@ -0,0 +1,6 @@
// WITH_RUNTIME
@Throws(Throwable::class)
fun a(b: Boolean) {
throw if (b) RuntimeException() else Exception()
}
@@ -0,0 +1,9 @@
// WITH_RUNTIME
class FooException : Exception()
class Test {
constructor() {
<caret>throw FooException()
}
}
@@ -0,0 +1,10 @@
// WITH_RUNTIME
class FooException : Exception()
class Test {
@Throws(FooException::class)
constructor() {
throw FooException()
}
}
@@ -0,0 +1,8 @@
// INTENTION_TEXT: "Add '@Throws' annotation"
// WITH_RUNTIME
class FooException : Exception()
fun test() {
<caret>throw FooException()
}
@@ -0,0 +1,9 @@
// INTENTION_TEXT: "Add '@Throws' annotation"
// WITH_RUNTIME
class FooException : Exception()
@Throws(FooException::class)
fun test() {
throw FooException()
}
@@ -0,0 +1,8 @@
// WITH_RUNTIME
class FooException : Exception()
class Test {
val getter: String
get() = <caret>throw FooException()
}
@@ -0,0 +1,9 @@
// WITH_RUNTIME
class FooException : Exception()
class Test {
val getter: String
@Throws(FooException::class)
get() = throw FooException()
}
@@ -0,0 +1,5 @@
// WITH_RUNTIME
@get:Throws(RuntimeException::class)
val a: String
get() = <caret>throw Exception()
@@ -0,0 +1,5 @@
// WITH_RUNTIME
@get:Throws(RuntimeException::class, Exception::class)
val a: String
get() = throw Exception()
@@ -0,0 +1,8 @@
// WITH_RUNTIME
// IS_APPLICABLE: false
class X {
init {
<caret>throw RuntimeException()
}
}
@@ -0,0 +1,4 @@
// WITH_RUNTIME
// IS_APPLICABLE: false
val f = { <caret>throw RuntimeException() }
@@ -0,0 +1,8 @@
// WITH_RUNTIME
class FooException : Exception()
class Test {
var setter: String = ""
set(value) = <caret>throw FooException()
}
@@ -0,0 +1,9 @@
// WITH_RUNTIME
class FooException : Exception()
class Test {
var setter: String = ""
@Throws(FooException::class)
set(value) = throw FooException()
}
@@ -0,0 +1,5 @@
// WITH_RUNTIME
@set:Throws(RuntimeException::class)
var setter: String = ""
set(value) = <caret>throw Exception()
@@ -0,0 +1,5 @@
// WITH_RUNTIME
@set:Throws(RuntimeException::class, Exception::class)
var setter: String = ""
set(value) = throw Exception()
+6
View File
@@ -0,0 +1,6 @@
// JS
// IS_APPLICABLE: false
fun a() {
<caret>throw RuntimeException()
}
@@ -0,0 +1,7 @@
// WITH_RUNTIME
// IS_APPLICABLE: false
fun a() {
class Ex : RuntimeException()
<caret>throw Ex()
}
@@ -1698,6 +1698,144 @@ public class IntentionTestGenerated extends AbstractIntentionTest {
}
}
@TestMetadata("idea/testData/intentions/addThrowsAnnotation")
@TestDataPath("$PROJECT_ROOT")
@RunWith(JUnit3RunnerWithInners.class)
public static class AddThrowsAnnotation extends AbstractIntentionTest {
private void runTest(String testDataFilePath) throws Exception {
KotlinTestUtils.runTest(this::doTest, TargetBackend.ANY, testDataFilePath);
}
public void testAllFilesPresentInAddThrowsAnnotation() throws Exception {
KotlinTestUtils.assertAllTestsPresentByMetadata(this.getClass(), new File("idea/testData/intentions/addThrowsAnnotation"), Pattern.compile("^([\\w\\-_]+)\\.(kt|kts)$"), TargetBackend.ANY, true);
}
@TestMetadata("fqName.kt")
public void testFqName() throws Exception {
runTest("idea/testData/intentions/addThrowsAnnotation/fqName.kt");
}
@TestMetadata("fqName2.kt")
public void testFqName2() throws Exception {
runTest("idea/testData/intentions/addThrowsAnnotation/fqName2.kt");
}
@TestMetadata("functionCall.kt")
public void testFunctionCall() throws Exception {
runTest("idea/testData/intentions/addThrowsAnnotation/functionCall.kt");
}
@TestMetadata("hasThrowsWithDifferentClassArgument.kt")
public void testHasThrowsWithDifferentClassArgument() throws Exception {
runTest("idea/testData/intentions/addThrowsAnnotation/hasThrowsWithDifferentClassArgument.kt");
}
@TestMetadata("hasThrowsWithDifferentClassArgument2.kt")
public void testHasThrowsWithDifferentClassArgument2() throws Exception {
runTest("idea/testData/intentions/addThrowsAnnotation/hasThrowsWithDifferentClassArgument2.kt");
}
@TestMetadata("hasThrowsWithDifferentClassArgument3.kt")
public void testHasThrowsWithDifferentClassArgument3() throws Exception {
runTest("idea/testData/intentions/addThrowsAnnotation/hasThrowsWithDifferentClassArgument3.kt");
}
@TestMetadata("hasThrowsWithDifferentClassArgument4.kt")
public void testHasThrowsWithDifferentClassArgument4() throws Exception {
runTest("idea/testData/intentions/addThrowsAnnotation/hasThrowsWithDifferentClassArgument4.kt");
}
@TestMetadata("hasThrowsWithDifferentClassArgument5.kt")
public void testHasThrowsWithDifferentClassArgument5() throws Exception {
runTest("idea/testData/intentions/addThrowsAnnotation/hasThrowsWithDifferentClassArgument5.kt");
}
@TestMetadata("hasThrowsWithEmptyArgument.kt")
public void testHasThrowsWithEmptyArgument() throws Exception {
runTest("idea/testData/intentions/addThrowsAnnotation/hasThrowsWithEmptyArgument.kt");
}
@TestMetadata("hasThrowsWithNoArgument.kt")
public void testHasThrowsWithNoArgument() throws Exception {
runTest("idea/testData/intentions/addThrowsAnnotation/hasThrowsWithNoArgument.kt");
}
@TestMetadata("hasThrowsWithSameClassArgument.kt")
public void testHasThrowsWithSameClassArgument() throws Exception {
runTest("idea/testData/intentions/addThrowsAnnotation/hasThrowsWithSameClassArgument.kt");
}
@TestMetadata("hasThrowsWithSameClassArgument2.kt")
public void testHasThrowsWithSameClassArgument2() throws Exception {
runTest("idea/testData/intentions/addThrowsAnnotation/hasThrowsWithSameClassArgument2.kt");
}
@TestMetadata("hasThrowsWithSameClassArgument3.kt")
public void testHasThrowsWithSameClassArgument3() throws Exception {
runTest("idea/testData/intentions/addThrowsAnnotation/hasThrowsWithSameClassArgument3.kt");
}
@TestMetadata("hasThrowsWithSameClassArgument4.kt")
public void testHasThrowsWithSameClassArgument4() throws Exception {
runTest("idea/testData/intentions/addThrowsAnnotation/hasThrowsWithSameClassArgument4.kt");
}
@TestMetadata("if.kt")
public void testIf() throws Exception {
runTest("idea/testData/intentions/addThrowsAnnotation/if.kt");
}
@TestMetadata("inConstructor.kt")
public void testInConstructor() throws Exception {
runTest("idea/testData/intentions/addThrowsAnnotation/inConstructor.kt");
}
@TestMetadata("inFunction.kt")
public void testInFunction() throws Exception {
runTest("idea/testData/intentions/addThrowsAnnotation/inFunction.kt");
}
@TestMetadata("inGetter.kt")
public void testInGetter() throws Exception {
runTest("idea/testData/intentions/addThrowsAnnotation/inGetter.kt");
}
@TestMetadata("inGetter2.kt")
public void testInGetter2() throws Exception {
runTest("idea/testData/intentions/addThrowsAnnotation/inGetter2.kt");
}
@TestMetadata("inInit.kt")
public void testInInit() throws Exception {
runTest("idea/testData/intentions/addThrowsAnnotation/inInit.kt");
}
@TestMetadata("inLambda.kt")
public void testInLambda() throws Exception {
runTest("idea/testData/intentions/addThrowsAnnotation/inLambda.kt");
}
@TestMetadata("inSetter.kt")
public void testInSetter() throws Exception {
runTest("idea/testData/intentions/addThrowsAnnotation/inSetter.kt");
}
@TestMetadata("inSetter2.kt")
public void testInSetter2() throws Exception {
runTest("idea/testData/intentions/addThrowsAnnotation/inSetter2.kt");
}
@TestMetadata("js.kt")
public void testJs() throws Exception {
runTest("idea/testData/intentions/addThrowsAnnotation/js.kt");
}
@TestMetadata("localException.kt")
public void testLocalException() throws Exception {
runTest("idea/testData/intentions/addThrowsAnnotation/localException.kt");
}
}
@TestMetadata("idea/testData/intentions/addValOrVar")
@TestDataPath("$PROJECT_ROOT")
@RunWith(JUnit3RunnerWithInners.class)