Add @JvmStatic Intention: Support functions/properties in objects

Also eliminate unnecessary object instance references on Java call sites

 #KT-20095 Fixed
This commit is contained in:
Alexey Sedunov
2017-11-03 15:39:16 +03:00
parent cfad305826
commit ba0b21d1a3
55 changed files with 446 additions and 109 deletions
@@ -50,23 +50,25 @@ fun Project.projectScope(): GlobalSearchScope = GlobalSearchScope.projectScope(t
fun PsiFile.fileScope(): GlobalSearchScope = GlobalSearchScope.fileScope(this)
fun GlobalSearchScope.restrictToKotlinSources() = GlobalSearchScope.getScopeRestrictedByFileTypes(this, KotlinFileType.INSTANCE)
fun GlobalSearchScope.restrictByFileType(fileType: FileType) = GlobalSearchScope.getScopeRestrictedByFileTypes(this, fileType)
fun SearchScope.restrictToKotlinSources(): SearchScope {
return when (this) {
is GlobalSearchScope -> restrictToKotlinSources()
is LocalSearchScope -> {
val ktElements = scope.filter { it.containingFile is KtFile }
when (ktElements.size) {
0 -> GlobalSearchScope.EMPTY_SCOPE
scope.size -> this
else -> LocalSearchScope(ktElements.toTypedArray())
}
fun SearchScope.restrictByFileType(fileType: FileType) = when (this) {
is GlobalSearchScope -> restrictByFileType(fileType)
is LocalSearchScope -> {
val elements = scope.filter { it.containingFile.fileType == fileType }
when (elements.size) {
0 -> GlobalSearchScope.EMPTY_SCOPE
scope.size -> this
else -> LocalSearchScope(elements.toTypedArray())
}
else -> this
}
else -> this
}
fun GlobalSearchScope.restrictToKotlinSources() = restrictByFileType(KotlinFileType.INSTANCE)
fun SearchScope.restrictToKotlinSources() = restrictByFileType(KotlinFileType.INSTANCE)
fun SearchScope.excludeKotlinSources(): SearchScope = excludeFileTypes(KotlinFileType.INSTANCE)
fun SearchScope.excludeFileTypes(vararg fileTypes: FileType): SearchScope {
@@ -1,5 +1,5 @@
<html>
<body>
This intention adds @JvmStatic annotation to main() method in an object.
This intention adds @JvmStatic annotation to the specified function or property of an object declaration.
</body>
</html>
@@ -17,32 +17,84 @@
package org.jetbrains.kotlin.idea.intentions
import com.intellij.codeInsight.intention.LowPriorityAction
import com.intellij.ide.highlighter.JavaFileType
import com.intellij.openapi.editor.Editor
import org.jetbrains.kotlin.descriptors.FunctionDescriptor
import org.jetbrains.kotlin.idea.MainFunctionDetector
import org.jetbrains.kotlin.idea.caches.resolve.resolveToDescriptorIfAny
import com.intellij.openapi.util.TextRange
import com.intellij.psi.PsiReferenceExpression
import com.intellij.psi.search.searches.ReferencesSearch
import org.jetbrains.kotlin.asJava.classes.KtLightClass
import org.jetbrains.kotlin.asJava.elements.KtLightElement
import org.jetbrains.kotlin.asJava.elements.KtLightField
import org.jetbrains.kotlin.idea.runSynchronouslyWithProgress
import org.jetbrains.kotlin.idea.search.restrictByFileType
import org.jetbrains.kotlin.idea.util.addAnnotation
import org.jetbrains.kotlin.idea.util.application.runReadAction
import org.jetbrains.kotlin.idea.util.application.runWriteAction
import org.jetbrains.kotlin.idea.util.findAnnotation
import org.jetbrains.kotlin.lexer.KtTokens
import org.jetbrains.kotlin.load.java.JvmAbi
import org.jetbrains.kotlin.name.FqName
import org.jetbrains.kotlin.psi.KtNamedDeclaration
import org.jetbrains.kotlin.psi.KtNamedFunction
import org.jetbrains.kotlin.psi.KtObjectDeclaration
import org.jetbrains.kotlin.psi.KtProperty
import org.jetbrains.kotlin.psi.psiUtil.containingClassOrObject
class AddJvmStaticIntention : SelfTargetingIntention<KtNamedFunction>(
KtNamedFunction::class.java,
"Add '@JvmStatic' annotation"
class AddJvmStaticIntention : SelfTargetingRangeIntention<KtNamedDeclaration>(
KtNamedDeclaration::class.java,
"Add '@JvmStatic' annotation"
), LowPriorityAction {
private val JvmStaticFqName = FqName("kotlin.jvm.JvmStatic")
private val JvmFieldFqName = FqName("kotlin.jvm.JvmField")
private val annotationFqName = FqName("kotlin.jvm.JvmStatic")
override fun startInWriteAction() = false
override fun isApplicableTo(element: KtNamedFunction, caretOffset: Int): Boolean {
if (element.findAnnotation(annotationFqName) != null) return false
if (element.isTopLevel) return false
val detector = MainFunctionDetector { function ->
function.resolveToDescriptorIfAny() as? FunctionDescriptor
override fun applicabilityRange(element: KtNamedDeclaration): TextRange? {
if (element !is KtNamedFunction && element !is KtProperty) return null
if (element.hasModifier(KtTokens.ABSTRACT_KEYWORD)) return null
if (element.hasModifier(KtTokens.OPEN_KEYWORD)) return null
if (element.hasModifier(KtTokens.OVERRIDE_KEYWORD)) return null
val containingObject = element.containingClassOrObject as? KtObjectDeclaration ?: return null
if (containingObject.isObjectLiteral()) return null
if (element is KtProperty) {
if (element.hasModifier(KtTokens.CONST_KEYWORD)) return null
if (element.findAnnotation(JvmFieldFqName) != null) return null
}
return detector.isMain(element, false)
if (element.findAnnotation(JvmStaticFqName) != null) return null
return element.nameIdentifier?.textRange
}
override fun applyTo(element: KtNamedFunction, editor: Editor?) {
element.addAnnotation(annotationFqName)
override fun applyTo(element: KtNamedDeclaration, editor: Editor?) {
val containingObject = element.containingClassOrObject as? KtObjectDeclaration ?: return
val isCompanionMember = containingObject.isCompanion()
val instanceFieldName = if (isCompanionMember) containingObject.name else JvmAbi.INSTANCE_FIELD
val instanceFieldContainingClass = if (isCompanionMember) (containingObject.containingClassOrObject ?: return) else containingObject
val project = element.project
val expressionsToReplaceWithQualifier =
project.runSynchronouslyWithProgress("Looking for usages in Java files...", true) {
runReadAction {
val searchScope = element.useScope.restrictByFileType(JavaFileType.INSTANCE)
ReferencesSearch
.search(element, searchScope)
.mapNotNull {
val refExpr = it.element as? PsiReferenceExpression ?: return@mapNotNull null
if ((refExpr.resolve() as? KtLightElement<*, *>)?.kotlinOrigin != element) return@mapNotNull null
val qualifierExpr = refExpr.qualifierExpression as? PsiReferenceExpression ?: return@mapNotNull null
if (qualifierExpr.qualifierExpression == null) return@mapNotNull null
val instanceField = qualifierExpr.resolve() as? KtLightField ?: return@mapNotNull null
if (instanceField.name != instanceFieldName) return@mapNotNull null
if ((instanceField.containingClass as? KtLightClass)?.kotlinOrigin != instanceFieldContainingClass) return@mapNotNull null
qualifierExpr
}
}
} ?: return
runWriteAction {
element.addAnnotation(JvmStaticFqName)
expressionsToReplaceWithQualifier.forEach { it.replace(it.qualifierExpression!!) }
}
}
}
+7
View File
@@ -0,0 +1,7 @@
// WITH_RUNTIME
// IS_APPLICABLE: false
// ERROR: Abstract property 'foo' in non-abstract class 'Test'
object Test {
abstract val <caret>foo: Int
}
+6
View File
@@ -0,0 +1,6 @@
// WITH_RUNTIME
// IS_APPLICABLE: false
object Test {
const val <caret>foo = 1
}
@@ -1,8 +0,0 @@
// WITH_RUNTIME
class Test {
companion object {
fun <caret>main(args: Array<String>) {
}
}
}
@@ -1,9 +0,0 @@
// WITH_RUNTIME
class Test {
companion object {
@JvmStatic
fun main(args: Array<String>) {
}
}
}
-6
View File
@@ -1,6 +0,0 @@
// WITH_RUNTIME
object Test {
fun <caret>main(args: Array<String>) {
}
}
-7
View File
@@ -1,7 +0,0 @@
// WITH_RUNTIME
object Test {
@JvmName("main")
fun <caret>test(args: Array<String>) {
}
}
@@ -1,8 +0,0 @@
// WITH_RUNTIME
object Test {
@JvmStatic
@JvmName("main")
fun <caret>test(args: Array<String>) {
}
}
@@ -1,8 +0,0 @@
// WITH_RUNTIME
// IS_APPLICABLE: false
object Test {
@JvmName("test")
fun <caret>main(args: Array<String>) {
}
}
@@ -1,6 +0,0 @@
// IS_APPLICABLE: false
object Test {
fun <caret>test(args: Array<String>) {
}
}
+6
View File
@@ -0,0 +1,6 @@
// WITH_RUNTIME
// IS_APPLICABLE: false
object Test {
open val <caret>foo = 1
}
+10
View File
@@ -0,0 +1,10 @@
// WITH_RUNTIME
// IS_APPLICABLE: false
interface I {
val foo: Int
}
object Test : I {
override val <caret>foo = 1
}
+3
View File
@@ -0,0 +1,3 @@
// IS_APPLICABLE: false
val <caret>foo = 1
@@ -0,0 +1,5 @@
// IS_APPLICABLE: false
private fun test() = object {
val <caret>foo = 1
}
+5
View File
@@ -0,0 +1,5 @@
// IS_APPLICABLE: false
class Test {
val <caret>foo = 1
}
@@ -0,0 +1,7 @@
// WITH_RUNTIME
// IS_APPLICABLE: false
object Test {
@JvmField
val <caret>foo = 1
}
@@ -1,7 +1,7 @@
// WITH_RUNTIME
// IS_APPLICABLE: false
object Test {
@JvmStatic
fun main(args: Array<String>) {
}
val <caret>foo = 1
}
@@ -0,0 +1,5 @@
{
"mainFile": "test/test.kt",
"intentionClass": "org.jetbrains.kotlin.idea.intentions.AddJvmStaticIntention",
"withRuntime": "true"
}
@@ -0,0 +1,8 @@
package test;
class J {
void test(C.Companion companion) {
companion.foo("x");
C.foo("y");
}
}
@@ -0,0 +1,8 @@
package test
class C {
companion object {
@JvmStatic
fun foo(s: String) {}
}
}
@@ -0,0 +1,8 @@
package test;
class J {
void test(C.Companion companion) {
companion.foo("x");
C.Companion.foo("y");
}
}
@@ -0,0 +1,7 @@
package test
class C {
companion object {
fun <caret>foo(s: String) {}
}
}
@@ -0,0 +1,5 @@
{
"mainFile": "test/test.kt",
"intentionClass": "org.jetbrains.kotlin.idea.intentions.AddJvmStaticIntention",
"withRuntime": "true"
}
@@ -0,0 +1,11 @@
package test;
class J {
void test(C.Companion companion) {
companion.getFoo();
companion.setFoo(1);
C.getFoo();
C.setFoo(2);
}
}
@@ -0,0 +1,8 @@
package test
class C {
companion object {
@JvmStatic
var foo: Int = 1
}
}
@@ -0,0 +1,11 @@
package test;
class J {
void test(C.Companion companion) {
companion.getFoo();
companion.setFoo(1);
C.Companion.getFoo();
C.Companion.setFoo(2);
}
}
@@ -0,0 +1,7 @@
package test
class C {
companion object {
var <caret>foo: Int = 1
}
}
@@ -0,0 +1,5 @@
{
"mainFile": "test/test.kt",
"intentionClass": "org.jetbrains.kotlin.idea.intentions.AddJvmStaticIntention",
"withRuntime": "true"
}
@@ -0,0 +1,8 @@
package test;
class J {
void test(C.O companion) {
companion.foo("x");
C.foo("y");
}
}
@@ -0,0 +1,8 @@
package test
class C {
companion object O {
@JvmStatic
fun foo(s: String) {}
}
}
@@ -0,0 +1,8 @@
package test;
class J {
void test(C.O companion) {
companion.foo("x");
C.O.foo("y");
}
}
@@ -0,0 +1,7 @@
package test
class C {
companion object O {
fun <caret>foo(s: String) {}
}
}
@@ -0,0 +1,5 @@
{
"mainFile": "test/test.kt",
"intentionClass": "org.jetbrains.kotlin.idea.intentions.AddJvmStaticIntention",
"withRuntime": "true"
}
@@ -0,0 +1,11 @@
package test;
class J {
void test(C.O companion) {
companion.getFoo();
companion.setFoo(1);
C.getFoo();
C.setFoo(2);
}
}
@@ -0,0 +1,8 @@
package test
class C {
companion object O {
@JvmStatic
var foo: Int = 1
}
}
@@ -0,0 +1,11 @@
package test;
class J {
void test(C.O companion) {
companion.getFoo();
companion.setFoo(1);
C.O.getFoo();
C.O.setFoo(2);
}
}
@@ -0,0 +1,7 @@
package test
class C {
companion object O {
var <caret>foo: Int = 1
}
}
@@ -0,0 +1,5 @@
{
"mainFile": "test/test.kt",
"intentionClass": "org.jetbrains.kotlin.idea.intentions.AddJvmStaticIntention",
"withRuntime": "true"
}
@@ -0,0 +1,8 @@
package test;
class J {
void test(O o) {
o.foo("x");
O.foo("y");
}
}
@@ -0,0 +1,6 @@
package test
object O {
@JvmStatic
fun foo(s: String) {}
}
@@ -0,0 +1,8 @@
package test;
class J {
void test(O o) {
o.foo("x");
O.INSTANCE.foo("y");
}
}
@@ -0,0 +1,5 @@
package test
object O {
fun <caret>foo(s: String) {}
}
@@ -0,0 +1,5 @@
{
"mainFile": "test/test.kt",
"intentionClass": "org.jetbrains.kotlin.idea.intentions.AddJvmStaticIntention",
"withRuntime": "true"
}
@@ -0,0 +1,11 @@
package test;
class J {
void test(O o) {
o.getFoo();
o.setFoo(1);
O.getFoo();
O.setFoo(2);
}
}
@@ -0,0 +1,6 @@
package test
object O {
@JvmStatic
var foo: Int = 1
}
@@ -0,0 +1,11 @@
package test;
class J {
void test(O o) {
o.getFoo();
o.setFoo(1);
O.INSTANCE.getFoo();
O.INSTANCE.setFoo(2);
}
}
@@ -0,0 +1,5 @@
package test
object O {
var <caret>foo: Int = 1
}
@@ -296,57 +296,81 @@ public class IntentionTestGenerated extends AbstractIntentionTest {
KotlinTestUtils.assertAllTestsPresentByMetadata(this.getClass(), new File("idea/testData/intentions/addJvmStatic"), Pattern.compile("^([\\w\\-_]+)\\.(kt|kts)$"), TargetBackend.ANY, true);
}
@TestMetadata("hasJvmStatic.kt")
public void testHasJvmStatic() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/intentions/addJvmStatic/hasJvmStatic.kt");
@TestMetadata("abstractVal.kt")
public void testAbstractVal() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/intentions/addJvmStatic/abstractVal.kt");
doTest(fileName);
}
@TestMetadata("inAnonymousObject.kt")
public void testInAnonymousObject() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/intentions/addJvmStatic/inAnonymousObject.kt");
@TestMetadata("constVal.kt")
public void testConstVal() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/intentions/addJvmStatic/constVal.kt");
doTest(fileName);
}
@TestMetadata("inClass.kt")
public void testInClass() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/intentions/addJvmStatic/inClass.kt");
@TestMetadata("funInAnonymousObject.kt")
public void testFunInAnonymousObject() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/intentions/addJvmStatic/funInAnonymousObject.kt");
doTest(fileName);
}
@TestMetadata("inCompanionObject.kt")
public void testInCompanionObject() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/intentions/addJvmStatic/inCompanionObject.kt");
@TestMetadata("funInClass.kt")
public void testFunInClass() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/intentions/addJvmStatic/funInClass.kt");
doTest(fileName);
}
@TestMetadata("inObjedct.kt")
public void testInObjedct() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/intentions/addJvmStatic/inObjedct.kt");
@TestMetadata("funWithJvmStatic.kt")
public void testFunWithJvmStatic() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/intentions/addJvmStatic/funWithJvmStatic.kt");
doTest(fileName);
}
@TestMetadata("inTopLevel.kt")
public void testInTopLevel() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/intentions/addJvmStatic/inTopLevel.kt");
@TestMetadata("openVal.kt")
public void testOpenVal() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/intentions/addJvmStatic/openVal.kt");
doTest(fileName);
}
@TestMetadata("mainJvmName.kt")
public void testMainJvmName() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/intentions/addJvmStatic/mainJvmName.kt");
@TestMetadata("overrideVal.kt")
public void testOverrideVal() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/intentions/addJvmStatic/overrideVal.kt");
doTest(fileName);
}
@TestMetadata("notMainJvmName.kt")
public void testNotMainJvmName() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/intentions/addJvmStatic/notMainJvmName.kt");
@TestMetadata("topLevelFun.kt")
public void testTopLevelFun() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/intentions/addJvmStatic/topLevelFun.kt");
doTest(fileName);
}
@TestMetadata("notMainMethod.kt")
public void testNotMainMethod() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/intentions/addJvmStatic/notMainMethod.kt");
@TestMetadata("topLevelVal.kt")
public void testTopLevelVal() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/intentions/addJvmStatic/topLevelVal.kt");
doTest(fileName);
}
@TestMetadata("valInAnonymousObject.kt")
public void testValInAnonymousObject() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/intentions/addJvmStatic/valInAnonymousObject.kt");
doTest(fileName);
}
@TestMetadata("valInClass.kt")
public void testValInClass() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/intentions/addJvmStatic/valInClass.kt");
doTest(fileName);
}
@TestMetadata("valWithJvmField.kt")
public void testValWithJvmField() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/intentions/addJvmStatic/valWithJvmField.kt");
doTest(fileName);
}
@TestMetadata("valWithJvmStatic.kt")
public void testValWithJvmStatic() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/intentions/addJvmStatic/valWithJvmStatic.kt");
doTest(fileName);
}
}
@@ -32,6 +32,42 @@ import java.util.regex.Pattern;
@TestDataPath("$PROJECT_ROOT")
@RunWith(JUnit3RunnerWithInners.class)
public class MultiFileIntentionTestGenerated extends AbstractMultiFileIntentionTest {
@TestMetadata("addJvmStaticToCompanionObjectFun/addJvmStaticToCompanionObjectFun.test")
public void testAddJvmStaticToCompanionObjectFun_AddJvmStaticToCompanionObjectFun() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/multiFileIntentions/addJvmStaticToCompanionObjectFun/addJvmStaticToCompanionObjectFun.test");
doTest(fileName);
}
@TestMetadata("addJvmStaticToCompanionObjectProperty/addJvmStaticToCompanionObjectProperty.test")
public void testAddJvmStaticToCompanionObjectProperty_AddJvmStaticToCompanionObjectProperty() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/multiFileIntentions/addJvmStaticToCompanionObjectProperty/addJvmStaticToCompanionObjectProperty.test");
doTest(fileName);
}
@TestMetadata("addJvmStaticToNamedCompanionObjectFun/addJvmStaticToNamedCompanionObjectFun.test")
public void testAddJvmStaticToNamedCompanionObjectFun_AddJvmStaticToNamedCompanionObjectFun() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/multiFileIntentions/addJvmStaticToNamedCompanionObjectFun/addJvmStaticToNamedCompanionObjectFun.test");
doTest(fileName);
}
@TestMetadata("addJvmStaticToNamedCompanionObjectProperty/addJvmStaticToNamedCompanionObjectProperty.test")
public void testAddJvmStaticToNamedCompanionObjectProperty_AddJvmStaticToNamedCompanionObjectProperty() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/multiFileIntentions/addJvmStaticToNamedCompanionObjectProperty/addJvmStaticToNamedCompanionObjectProperty.test");
doTest(fileName);
}
@TestMetadata("addJvmStaticToObjectFun/addJvmStaticToObjectFun.test")
public void testAddJvmStaticToObjectFun_AddJvmStaticToObjectFun() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/multiFileIntentions/addJvmStaticToObjectFun/addJvmStaticToObjectFun.test");
doTest(fileName);
}
@TestMetadata("addJvmStaticToObjectProperty/addJvmStaticToObjectProperty.test")
public void testAddJvmStaticToObjectProperty_AddJvmStaticToObjectProperty() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/multiFileIntentions/addJvmStaticToObjectProperty/addJvmStaticToObjectProperty.test");
doTest(fileName);
}
public void testAllFilesPresentInMultiFileIntentions() throws Exception {
KotlinTestUtils.assertAllTestsPresentInSingleGeneratedClass(this.getClass(), new File("idea/testData/multiFileIntentions"), Pattern.compile("^(.+)\\.test$"), TargetBackend.ANY);
}