Fix nullability quick-fixes with implicit receiver #KT-17726 Fixed

This commit is contained in:
Dmitry Neverov
2017-05-21 07:07:43 +02:00
committed by Mikhail Glukhikh
parent 9d2ae54d2c
commit 46aaee5d05
44 changed files with 479 additions and 23 deletions
@@ -38,6 +38,10 @@ import org.jetbrains.kotlin.types.TypeUtils
import org.jetbrains.kotlin.types.typeUtil.isSubtypeOf
import org.jetbrains.kotlin.util.OperatorNameConventions
import org.jetbrains.kotlin.util.isValidOperator
import org.jetbrains.kotlin.psi.KtPsiFactory
import org.jetbrains.kotlin.resolve.calls.callUtil.getResolvedCall
import org.jetbrains.kotlin.resolve.calls.resolvedCallUtil.getImplicitReceiverValue
abstract class ExclExclCallFix(psiElement: PsiElement) : KotlinQuickFixAction<PsiElement>(psiElement) {
override fun getFamilyName(): String = text
@@ -75,7 +79,9 @@ class RemoveExclExclCallFix(psiElement: PsiElement) : ExclExclCallFix(psiElement
}
}
class AddExclExclCallFix(psiElement: PsiElement) : ExclExclCallFix(psiElement) {
class AddExclExclCallFix(psiElement: PsiElement, val checkImplicitReceivers: Boolean) : ExclExclCallFix(psiElement) {
constructor(psiElement: PsiElement) : this(psiElement, true)
override fun getText() = KotlinBundle.message("introduce.non.null.assertion")
override fun isAvailable(project: Project, editor: Editor?, file: PsiFile): Boolean
@@ -85,40 +91,50 @@ class AddExclExclCallFix(psiElement: PsiElement) : ExclExclCallFix(psiElement) {
override fun invoke(project: Project, editor: Editor?, file: KtFile) {
if (!FileModificationService.getInstance().prepareFileForWrite(file)) return
val modifiedExpression = getExpressionForIntroduceCall() ?: return
val exclExclExpression = KtPsiFactory(project).createExpressionByPattern("$0!!", modifiedExpression)
val expr = getExpressionForIntroduceCall() ?: return
val modifiedExpression = expr.expression
val exclExclExpression = if (expr.implicitReceiver) {
KtPsiFactory(project).createExpressionByPattern("this!!.$0", modifiedExpression)
} else {
KtPsiFactory(project).createExpressionByPattern("$0!!", modifiedExpression)
}
modifiedExpression.replace(exclExclExpression)
}
private fun getExpressionForIntroduceCall(): KtExpression? {
private class ExpressionForCall(val expression: KtExpression, val implicitReceiver: Boolean)
private fun KtExpression?.expressionForCall(implicitReceiver: Boolean = false) = this?.let { ExpressionForCall(it, implicitReceiver) }
private fun getExpressionForIntroduceCall(): ExpressionForCall? {
val psiElement = element ?: return null
if (psiElement is LeafPsiElement && psiElement.elementType == KtTokens.DOT) {
val sibling = psiElement.prevSibling
if (sibling is KtExpression) {
return sibling
}
return if (psiElement is LeafPsiElement && psiElement.elementType == KtTokens.DOT) {
(psiElement.prevSibling as? KtExpression).expressionForCall()
}
else if (psiElement is KtArrayAccessExpression) {
return psiElement.arrayExpression
psiElement.arrayExpression.expressionForCall()
}
else if (psiElement is KtOperationReferenceExpression) {
val parent = psiElement.parent
return when (parent) {
is KtUnaryExpression -> parent.baseExpression
is KtBinaryExpression -> parent.left
when (parent) {
is KtUnaryExpression -> parent.baseExpression.expressionForCall()
is KtBinaryExpression -> parent.left.expressionForCall()
else -> null
}
}
else if (psiElement is KtExpression) {
return psiElement
if (checkImplicitReceivers && psiElement.getResolvedCall(psiElement.analyze())?.getImplicitReceiverValue() != null) {
val expressionToReplace = psiElement.parent as? KtCallExpression ?: psiElement
expressionToReplace.expressionForCall(implicitReceiver = true)
}
else psiElement.expressionForCall()
}
else {
null
}
return null
}
companion object : KotlinSingleIntentionActionFactory() {
override fun createAction(diagnostic: Diagnostic): IntentionAction
= AddExclExclCallFix(diagnostic.psiElement)
override fun createAction(diagnostic: Diagnostic): IntentionAction = AddExclExclCallFix(diagnostic.psiElement)
}
}
@@ -137,7 +153,7 @@ object SmartCastImpossibleExclExclFixFactory: KotlinSingleIntentionActionFactory
val nullableExpectedType = TypeUtils.makeNullable(expectedType)
if (!type.isSubtypeOf(nullableExpectedType)) return null
return AddExclExclCallFix(element)
return AddExclExclCallFix(element, checkImplicitReceivers = false)
}
}
@@ -21,8 +21,11 @@ import com.intellij.openapi.editor.Editor
import com.intellij.openapi.project.Project
import com.intellij.psi.PsiFile
import org.jetbrains.kotlin.diagnostics.Diagnostic
import org.jetbrains.kotlin.idea.caches.resolve.analyze
import org.jetbrains.kotlin.psi.*
import org.jetbrains.kotlin.psi.psiUtil.getParentOfType
import org.jetbrains.kotlin.resolve.calls.callUtil.getResolvedCall
import org.jetbrains.kotlin.resolve.calls.resolvedCallUtil.getImplicitReceiverValue
abstract class ReplaceCallFix(
expression: KtQualifiedExpression,
@@ -44,14 +47,36 @@ abstract class ReplaceCallFix(
}
}
class ReplaceImplicitReceiverCallFix(expression: KtExpression) : KotlinQuickFixAction<KtExpression>(expression) {
override fun getFamilyName() = text
override fun getText() = "Replace with safe (this?.) call"
override fun invoke(project: Project, editor: Editor?, file: KtFile) {
val element = element ?: return
val newExpression = KtPsiFactory(element).createExpressionByPattern("this?.$0", element)
element.replace(newExpression)
}
}
class ReplaceWithSafeCallFix(expression: KtDotQualifiedExpression): ReplaceCallFix(expression, "?.") {
override fun getText() = "Replace with safe (?.) call"
companion object : KotlinSingleIntentionActionFactory() {
override fun createAction(diagnostic: Diagnostic): IntentionAction? {
val qualifiedExpression = diagnostic.psiElement.getParentOfType<KtDotQualifiedExpression>(strict = false) ?: return null
return ReplaceWithSafeCallFix(qualifiedExpression)
val psiElement = diagnostic.psiElement
val qualifiedExpression = psiElement.parent as? KtDotQualifiedExpression
if (qualifiedExpression != null) {
return ReplaceWithSafeCallFix(qualifiedExpression)
} else {
psiElement as? KtNameReferenceExpression ?: return null
if (psiElement.getResolvedCall(psiElement.analyze())?.getImplicitReceiverValue() != null) {
val expressionToReplace: KtExpression = psiElement.parent as? KtCallExpression ?: psiElement
return ReplaceImplicitReceiverCallFix(expressionToReplace)
}
return null
}
}
}
}
@@ -25,9 +25,9 @@ import org.jetbrains.kotlin.idea.intentions.OperatorToFunctionIntention
import org.jetbrains.kotlin.lexer.KtTokens
import org.jetbrains.kotlin.psi.*
import org.jetbrains.kotlin.psi.psiUtil.getAssignmentByLHS
import org.jetbrains.kotlin.resolve.BindingContext
import org.jetbrains.kotlin.resolve.calls.callUtil.getResolvedCall
import org.jetbrains.kotlin.resolve.calls.resolvedCallUtil.getImplicitReceiverValue
import org.jetbrains.kotlin.types.expressions.OperatorConventions
import org.jetbrains.kotlin.types.typeUtil.isBoolean
class ReplaceInfixOrOperatorCallFix(element: KtExpression) : KotlinQuickFixAction<KtExpression>(element) {
@@ -99,6 +99,7 @@ class ReplaceInfixOrOperatorCallFix(element: KtExpression) : KotlinQuickFixActio
is KtCallExpression -> {
if (parent.calleeExpression == null) null
else if (parent.parent is KtQualifiedExpression) null
else if (parent.getResolvedCall(parent.analyze())?.getImplicitReceiverValue() != null) null
else ReplaceInfixOrOperatorCallFix(parent)
}
else -> null
+5
View File
@@ -0,0 +1,5 @@
// "Add non-null asserted (!!) call" "true"
fun String?.foo() {
<caret>length
}
@@ -0,0 +1,5 @@
// "Add non-null asserted (!!) call" "true"
fun String?.foo() {
this!!.length
}
@@ -0,0 +1,5 @@
// "Add non-null asserted (!!) call" "true"
// WITH_RUNTIME
fun String?.foo() {
<caret>toLowerCase()
}
@@ -0,0 +1,5 @@
// "Add non-null asserted (!!) call" "true"
// WITH_RUNTIME
fun String?.foo() {
this!!.toLowerCase()
}
+5
View File
@@ -0,0 +1,5 @@
// "Add non-null asserted (!!) call" "true"
fun foo(a: String?) {
a<caret>.length
}
@@ -0,0 +1,5 @@
// "Add non-null asserted (!!) call" "true"
fun foo(a: String?) {
a!!.length
}
+6
View File
@@ -0,0 +1,6 @@
// "Add non-null asserted (!!) call" "true"
// WITH_RUNTIME
fun foo(a: String?) {
a<caret>.toLowerCase()
}
@@ -0,0 +1,6 @@
// "Add non-null asserted (!!) call" "true"
// WITH_RUNTIME
fun foo(a: String?) {
a!!.toLowerCase()
}
@@ -0,0 +1,9 @@
// "Replace with safe (?.) call" "false"
// ACTION: Add non-null asserted (!!) call
// ACTION: Convert to block body
// ACTION: Replace overloaded operator with function call
// ACTION: Replace with safe (this?.) call
// ACTION: Wrap with '?.let { ... }' call
// ERROR: Only safe (?.) or non-null asserted (!!.) calls are allowed on a nullable receiver of type String?
fun String?.foo(exec: (String.() -> Unit)) = exec<caret>()
@@ -0,0 +1,6 @@
// "Replace with safe (?.) call" "true"
// WITH_RUNTIME
fun foo(array: Array<String>?) {
array<caret>[0]
}
@@ -0,0 +1,6 @@
// "Replace with safe (?.) call" "true"
// WITH_RUNTIME
fun foo(array: Array<String>?) {
array?.get(0)
}
@@ -0,0 +1,6 @@
// "Replace with safe (?.) call" "true"
// WITH_RUNTIME
fun foo(array: Array<String>?) {
array<caret>[0] = ""
}
@@ -0,0 +1,6 @@
// "Replace with safe (?.) call" "true"
// WITH_RUNTIME
fun foo(array: Array<String>?) {
array?.set(0, "")
}
@@ -0,0 +1,6 @@
// "Replace with safe (?.) call" "true"
// WITH_RUNTIME
fun foo(bar: Int?) {
bar +<caret> 1
}
@@ -0,0 +1,6 @@
// "Replace with safe (?.) call" "true"
// WITH_RUNTIME
fun foo(bar: Int?) {
bar?.plus(1)
}
@@ -0,0 +1,9 @@
// "Replace with safe (?.) call" "true"
// WITH_RUNTIME
fun foo() {}
fun bar() {
val fff: (() -> Unit)? = ::foo
<caret>fff()
}
@@ -0,0 +1,9 @@
// "Replace with safe (?.) call" "true"
// WITH_RUNTIME
fun foo() {}
fun bar() {
val fff: (() -> Unit)? = ::foo
fff?.invoke()
}
@@ -0,0 +1,6 @@
// "Replace with safe (?.) call" "true"
// WITH_RUNTIME
fun foo(list: List<String>?) {
list<caret>[0]
}
@@ -0,0 +1,6 @@
// "Replace with safe (?.) call" "true"
// WITH_RUNTIME
fun foo(list: List<String>?) {
list?.get(0)
}
@@ -0,0 +1,5 @@
// "Replace with dot call" "true"
// WITH_RUNTIME
fun foo(a: String) {
a<caret>?.toLowerCase()
}
@@ -0,0 +1,5 @@
// "Replace with dot call" "true"
// WITH_RUNTIME
fun foo(a: String) {
a.toLowerCase()
}
+5
View File
@@ -0,0 +1,5 @@
// "Replace with dot call" "true"
// WITH_RUNTIME
fun foo(a: String) {
a<caret>?.length
}
@@ -0,0 +1,5 @@
// "Replace with dot call" "true"
// WITH_RUNTIME
fun foo(a: String) {
a.length
}
+7
View File
@@ -0,0 +1,7 @@
// "Replace with safe (?.) call" "true"
// WITH_RUNTIME
fun foo(a: String?) {
a.apply {
this<caret>.length
}
}
@@ -0,0 +1,7 @@
// "Replace with safe (?.) call" "true"
// WITH_RUNTIME
fun foo(a: String?) {
a.apply {
this?.length
}
}
@@ -0,0 +1,7 @@
// "Replace with safe (this?.) call" "true"
// WITH_RUNTIME
fun foo(a: String?) {
a.apply {
<caret>length
}
}
@@ -0,0 +1,7 @@
// "Replace with safe (this?.) call" "true"
// WITH_RUNTIME
fun foo(a: String?) {
a.apply {
this?.length
}
}
@@ -0,0 +1,7 @@
// "Replace with safe (this?.) call" "true"
// WITH_RUNTIME
fun foo(a: String?) {
a.apply {
<caret>toLowerCase()
}
}
@@ -0,0 +1,7 @@
// "Replace with safe (this?.) call" "true"
// WITH_RUNTIME
fun foo(a: String?) {
a.apply {
this?.toLowerCase()
}
}
@@ -0,0 +1,5 @@
// "Replace with safe (this?.) call" "true"
// WITH_RUNTIME
fun String?.foo() {
<caret>toLowerCase()
}
@@ -0,0 +1,5 @@
// "Replace with safe (this?.) call" "true"
// WITH_RUNTIME
fun String?.foo() {
this?.toLowerCase()
}
@@ -0,0 +1,5 @@
// "Replace with safe (?.) call" "true"
// WITH_RUNTIME
fun foo(a: String?) {
a<caret>.toLowerCase()
}
@@ -0,0 +1,5 @@
// "Replace with safe (?.) call" "true"
// WITH_RUNTIME
fun foo(a: String?) {
a?.toLowerCase()
}
+7
View File
@@ -0,0 +1,7 @@
// "Replace with safe (?.) call" "true"
// WITH_RUNTIME
fun foo(a: String?) {
a.let {
it<caret>.length
}
}
@@ -0,0 +1,7 @@
// "Replace with safe (?.) call" "true"
// WITH_RUNTIME
fun foo(a: String?) {
a.let {
it?.length
}
}
@@ -0,0 +1,7 @@
// "Replace with safe (?.) call" "true"
// WITH_RUNTIME
fun foo(a: String?) {
a.let { b ->
b<caret>.length
}
}
@@ -0,0 +1,7 @@
// "Replace with safe (?.) call" "true"
// WITH_RUNTIME
fun foo(a: String?) {
a.let { b ->
b?.length
}
}
@@ -0,0 +1,15 @@
// "Replace with safe (?.) call" "false"
// ACTION: Add non-null asserted (!!) call
// ACTION: Convert to expression body
// ACTION: Replace with safe (this?.) call
// ACTION: Wrap with '?.let { ... }' call
// ERROR: Only safe (?.) or non-null asserted (!!.) calls are allowed on a nullable receiver of type A?
class A {
fun foo() {
}
}
fun A?.bar() {
<caret>foo()
}
+5
View File
@@ -0,0 +1,5 @@
// "Replace with safe (?.) call" "true"
// WITH_RUNTIME
fun foo(a: String?) {
a<caret>.length
}
@@ -0,0 +1,5 @@
// "Replace with safe (?.) call" "true"
// WITH_RUNTIME
fun foo(a: String?) {
a?.length
}
@@ -234,6 +234,39 @@ public class QuickFixTestGenerated extends AbstractQuickFixTest {
}
}
@TestMetadata("idea/testData/quickfix/addExclExclCall")
@TestDataPath("$PROJECT_ROOT")
@RunWith(JUnit3RunnerWithInners.class)
public static class AddExclExclCall extends AbstractQuickFixTest {
public void testAllFilesPresentInAddExclExclCall() throws Exception {
KotlinTestUtils.assertAllTestsPresentByMetadata(this.getClass(), new File("idea/testData/quickfix/addExclExclCall"), Pattern.compile("^([\\w\\-_]+)\\.kt$"), TargetBackend.ANY, true);
}
@TestMetadata("implicit.kt")
public void testImplicit() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/quickfix/addExclExclCall/implicit.kt");
doTest(fileName);
}
@TestMetadata("implicitFunctionCall.kt")
public void testImplicitFunctionCall() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/quickfix/addExclExclCall/implicitFunctionCall.kt");
doTest(fileName);
}
@TestMetadata("normal.kt")
public void testNormal() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/quickfix/addExclExclCall/normal.kt");
doTest(fileName);
}
@TestMetadata("normal2.kt")
public void testNormal2() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/quickfix/addExclExclCall/normal2.kt");
doTest(fileName);
}
}
@TestMetadata("idea/testData/quickfix/addGenericUpperBound")
@TestDataPath("$PROJECT_ROOT")
@RunWith(JUnit3RunnerWithInners.class)
@@ -7243,6 +7276,12 @@ public class QuickFixTestGenerated extends AbstractQuickFixTest {
doTest(fileName);
}
@TestMetadata("unsafeInvokeWithImplicitReceiver.kt")
public void testUnsafeInvokeWithImplicitReceiver() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/quickfix/nullables/unsafeInfixCall/unsafeInvokeWithImplicitReceiver.kt");
doTest(fileName);
}
@TestMetadata("unsafeInvokeWithReceiver.kt")
public void testUnsafeInvokeWithReceiver() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/quickfix/nullables/unsafeInfixCall/unsafeInvokeWithReceiver.kt");
@@ -8427,6 +8466,45 @@ public class QuickFixTestGenerated extends AbstractQuickFixTest {
}
}
@TestMetadata("idea/testData/quickfix/replaceInfixOrOperatorCall")
@TestDataPath("$PROJECT_ROOT")
@RunWith(JUnit3RunnerWithInners.class)
public static class ReplaceInfixOrOperatorCall extends AbstractQuickFixTest {
public void testAllFilesPresentInReplaceInfixOrOperatorCall() throws Exception {
KotlinTestUtils.assertAllTestsPresentByMetadata(this.getClass(), new File("idea/testData/quickfix/replaceInfixOrOperatorCall"), Pattern.compile("^([\\w\\-_]+)\\.kt$"), TargetBackend.ANY, true);
}
@TestMetadata("array.kt")
public void testArray() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/quickfix/replaceInfixOrOperatorCall/array.kt");
doTest(fileName);
}
@TestMetadata("arraySet.kt")
public void testArraySet() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/quickfix/replaceInfixOrOperatorCall/arraySet.kt");
doTest(fileName);
}
@TestMetadata("binaryOperator.kt")
public void testBinaryOperator() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/quickfix/replaceInfixOrOperatorCall/binaryOperator.kt");
doTest(fileName);
}
@TestMetadata("callExpression.kt")
public void testCallExpression() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/quickfix/replaceInfixOrOperatorCall/callExpression.kt");
doTest(fileName);
}
@TestMetadata("list.kt")
public void testList() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/quickfix/replaceInfixOrOperatorCall/list.kt");
doTest(fileName);
}
}
@TestMetadata("idea/testData/quickfix/replaceJvmFieldWithConst")
@TestDataPath("$PROJECT_ROOT")
@RunWith(JUnit3RunnerWithInners.class)
@@ -8490,6 +8568,90 @@ public class QuickFixTestGenerated extends AbstractQuickFixTest {
}
}
@TestMetadata("idea/testData/quickfix/replaceWithDotCall")
@TestDataPath("$PROJECT_ROOT")
@RunWith(JUnit3RunnerWithInners.class)
public static class ReplaceWithDotCall extends AbstractQuickFixTest {
public void testAllFilesPresentInReplaceWithDotCall() throws Exception {
KotlinTestUtils.assertAllTestsPresentByMetadata(this.getClass(), new File("idea/testData/quickfix/replaceWithDotCall"), Pattern.compile("^([\\w\\-_]+)\\.kt$"), TargetBackend.ANY, true);
}
@TestMetadata("functionCall.kt")
public void testFunctionCall() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/quickfix/replaceWithDotCall/functionCall.kt");
doTest(fileName);
}
@TestMetadata("normal.kt")
public void testNormal() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/quickfix/replaceWithDotCall/normal.kt");
doTest(fileName);
}
}
@TestMetadata("idea/testData/quickfix/replaceWithSafeCall")
@TestDataPath("$PROJECT_ROOT")
@RunWith(JUnit3RunnerWithInners.class)
public static class ReplaceWithSafeCall extends AbstractQuickFixTest {
public void testAllFilesPresentInReplaceWithSafeCall() throws Exception {
KotlinTestUtils.assertAllTestsPresentByMetadata(this.getClass(), new File("idea/testData/quickfix/replaceWithSafeCall"), Pattern.compile("^([\\w\\-_]+)\\.kt$"), TargetBackend.ANY, true);
}
@TestMetadata("apply.kt")
public void testApply() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/quickfix/replaceWithSafeCall/apply.kt");
doTest(fileName);
}
@TestMetadata("applyWithImplicitParameter.kt")
public void testApplyWithImplicitParameter() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/quickfix/replaceWithSafeCall/applyWithImplicitParameter.kt");
doTest(fileName);
}
@TestMetadata("applyWithImplicitParameterFunctionCall.kt")
public void testApplyWithImplicitParameterFunctionCall() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/quickfix/replaceWithSafeCall/applyWithImplicitParameterFunctionCall.kt");
doTest(fileName);
}
@TestMetadata("extFunction.kt")
public void testExtFunction() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/quickfix/replaceWithSafeCall/extFunction.kt");
doTest(fileName);
}
@TestMetadata("functionCall.kt")
public void testFunctionCall() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/quickfix/replaceWithSafeCall/functionCall.kt");
doTest(fileName);
}
@TestMetadata("let.kt")
public void testLet() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/quickfix/replaceWithSafeCall/let.kt");
doTest(fileName);
}
@TestMetadata("letWithParameter.kt")
public void testLetWithParameter() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/quickfix/replaceWithSafeCall/letWithParameter.kt");
doTest(fileName);
}
@TestMetadata("noReplaceWithSafeCallForImplicitReceiver.kt")
public void testNoReplaceWithSafeCallForImplicitReceiver() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/quickfix/replaceWithSafeCall/noReplaceWithSafeCallForImplicitReceiver.kt");
doTest(fileName);
}
@TestMetadata("normal.kt")
public void testNormal() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/quickfix/replaceWithSafeCall/normal.kt");
doTest(fileName);
}
}
@TestMetadata("idea/testData/quickfix/simplifyComparison")
@TestDataPath("$PROJECT_ROOT")
@RunWith(JUnit3RunnerWithInners.class)