Introduce Variable: Support destructuring declarations
#KT-6262
This commit is contained in:
@@ -199,6 +199,10 @@ public class KtPsiFactory(private val project: Project) {
|
||||
return getter
|
||||
}
|
||||
|
||||
public fun createMultiDeclaration(text: String): KtMultiDeclaration {
|
||||
return (createFunction("fun foo() {$text}").bodyExpression as KtBlockExpression).statements.first() as KtMultiDeclaration
|
||||
}
|
||||
|
||||
public fun <TDeclaration : KtDeclaration> createDeclaration(text: String): TDeclaration {
|
||||
val file = createFile(text)
|
||||
val declarations = file.getDeclarations()
|
||||
|
||||
+140
-44
@@ -16,30 +16,31 @@
|
||||
|
||||
package org.jetbrains.kotlin.idea.refactoring.introduce.introduceVariable
|
||||
|
||||
import com.intellij.codeInsight.lookup.LookupElementBuilder
|
||||
import com.intellij.codeInsight.template.*
|
||||
import com.intellij.openapi.actionSystem.DataContext
|
||||
import com.intellij.openapi.application.ApplicationManager
|
||||
import com.intellij.openapi.command.impl.FinishMarkAction
|
||||
import com.intellij.openapi.command.impl.StartMarkAction
|
||||
import com.intellij.openapi.editor.Editor
|
||||
import com.intellij.openapi.project.Project
|
||||
import com.intellij.openapi.ui.Messages
|
||||
import com.intellij.openapi.util.Key
|
||||
import com.intellij.openapi.util.TextRange
|
||||
import com.intellij.psi.PsiDocumentManager
|
||||
import com.intellij.psi.PsiElement
|
||||
import com.intellij.psi.PsiFile
|
||||
import com.intellij.psi.PsiWhiteSpace
|
||||
import com.intellij.psi.*
|
||||
import com.intellij.psi.util.PsiTreeUtil
|
||||
import com.intellij.refactoring.HelpID
|
||||
import com.intellij.refactoring.introduce.inplace.OccurrencesChooser
|
||||
import com.intellij.refactoring.util.CommonRefactoringUtil
|
||||
import com.intellij.util.SmartList
|
||||
import org.jetbrains.kotlin.builtins.KotlinBuiltIns
|
||||
import org.jetbrains.kotlin.descriptors.FunctionDescriptor
|
||||
import org.jetbrains.kotlin.descriptors.ValueParameterDescriptor
|
||||
import org.jetbrains.kotlin.idea.analysis.analyzeInContext
|
||||
import org.jetbrains.kotlin.idea.analysis.computeTypeInfoInContext
|
||||
import org.jetbrains.kotlin.idea.caches.resolve.getResolutionFacade
|
||||
import org.jetbrains.kotlin.idea.core.KotlinNameSuggester
|
||||
import org.jetbrains.kotlin.idea.core.NewDeclarationNameValidator
|
||||
import org.jetbrains.kotlin.idea.core.compareDescriptors
|
||||
import org.jetbrains.kotlin.idea.core.moveInsideParenthesesAndReplaceWith
|
||||
import org.jetbrains.kotlin.idea.codeInsight.DescriptorToSourceUtilsIde
|
||||
import org.jetbrains.kotlin.idea.core.*
|
||||
import org.jetbrains.kotlin.idea.core.refactoring.Pass
|
||||
import org.jetbrains.kotlin.idea.core.refactoring.chooseContainerElementIfNecessary
|
||||
import org.jetbrains.kotlin.idea.core.refactoring.removeTemplateEntryBracesIfPossible
|
||||
@@ -51,6 +52,7 @@ import org.jetbrains.kotlin.idea.resolve.ResolutionFacade
|
||||
import org.jetbrains.kotlin.idea.util.IdeDescriptorRenderers
|
||||
import org.jetbrains.kotlin.idea.util.ShortenReferences
|
||||
import org.jetbrains.kotlin.idea.util.application.executeCommand
|
||||
import org.jetbrains.kotlin.idea.util.application.executeWriteCommand
|
||||
import org.jetbrains.kotlin.idea.util.application.runWriteAction
|
||||
import org.jetbrains.kotlin.idea.util.getResolutionScope
|
||||
import org.jetbrains.kotlin.idea.util.psi.patternMatching.KotlinPsiUnifier
|
||||
@@ -68,6 +70,7 @@ import org.jetbrains.kotlin.resolve.source.getPsi
|
||||
import org.jetbrains.kotlin.types.KotlinType
|
||||
import org.jetbrains.kotlin.types.checker.KotlinTypeChecker
|
||||
import org.jetbrains.kotlin.utils.addIfNotNull
|
||||
import org.jetbrains.kotlin.utils.addToStdlib.singletonList
|
||||
import org.jetbrains.kotlin.utils.ifEmpty
|
||||
import org.jetbrains.kotlin.utils.sure
|
||||
import java.util.*
|
||||
@@ -83,19 +86,20 @@ object KotlinIntroduceVariableHandler : KotlinIntroduceHandlerBase() {
|
||||
|
||||
private class IntroduceVariableContext(
|
||||
private val expression: KtExpression,
|
||||
private val nameSuggestion: String,
|
||||
private val nameSuggestions: List<Collection<String>>,
|
||||
private val allReplaces: List<KtExpression>,
|
||||
private val commonContainer: PsiElement,
|
||||
private val commonParent: PsiElement,
|
||||
private val replaceOccurrence: Boolean,
|
||||
private val noTypeInference: Boolean,
|
||||
private val expressionType: KotlinType?,
|
||||
private val componentFunctions: List<FunctionDescriptor>,
|
||||
private val bindingContext: BindingContext,
|
||||
private val resolutionFacade: ResolutionFacade
|
||||
) {
|
||||
private val psiFactory = KtPsiFactory(expression)
|
||||
|
||||
var propertyRef: KtProperty? = null
|
||||
var propertyRef: KtDeclaration? = null
|
||||
var reference: KtExpression? = null
|
||||
val references = ArrayList<KtExpression>()
|
||||
|
||||
@@ -106,7 +110,7 @@ object KotlinIntroduceVariableHandler : KotlinIntroduceHandlerBase() {
|
||||
private fun replaceExpression(expressionToReplace: KtExpression, addToReferences: Boolean): KtExpression {
|
||||
val isActualExpression = expression == expressionToReplace
|
||||
|
||||
val replacement = psiFactory.createExpression(nameSuggestion)
|
||||
val replacement = psiFactory.createExpression(nameSuggestions.single().first())
|
||||
val substringInfo = expressionToReplace.extractableSubstringInfo
|
||||
var result = when {
|
||||
expressionToReplace.isFunctionLiteralOutsideParentheses() -> {
|
||||
@@ -133,22 +137,31 @@ object KotlinIntroduceVariableHandler : KotlinIntroduceHandlerBase() {
|
||||
commonParent: PsiElement,
|
||||
allReplaces: List<KtExpression>
|
||||
) {
|
||||
val variableText = StringBuilder("val ").apply {
|
||||
append(nameSuggestion)
|
||||
if (noTypeInference) {
|
||||
val typeToRender = expressionType ?: resolutionFacade.moduleDescriptor.builtIns.anyType
|
||||
append(": ").append(IdeDescriptorRenderers.SOURCE_CODE.renderType(typeToRender))
|
||||
}
|
||||
append(" = ")
|
||||
|
||||
append(((expression as? KtParenthesizedExpression)?.expression ?: expression).text)
|
||||
}.toString()
|
||||
var property = psiFactory.createProperty(variableText)
|
||||
val initializer = (expression as? KtParenthesizedExpression)?.expression ?: expression
|
||||
var property: KtDeclaration = if (componentFunctions.isNotEmpty()) {
|
||||
buildString {
|
||||
componentFunctions.indices.joinTo(this, prefix = "val (", postfix = ")") { nameSuggestions[it].first() }
|
||||
append(" = ")
|
||||
append(initializer.text)
|
||||
}.let { psiFactory.createMultiDeclaration(it) }
|
||||
}
|
||||
else {
|
||||
buildString {
|
||||
append("val ")
|
||||
append(nameSuggestions.single().first())
|
||||
if (noTypeInference) {
|
||||
val typeToRender = expressionType ?: resolutionFacade.moduleDescriptor.builtIns.anyType
|
||||
append(": ").append(IdeDescriptorRenderers.SOURCE_CODE.renderType(typeToRender))
|
||||
}
|
||||
append(" = ")
|
||||
append(initializer.text)
|
||||
}.let { psiFactory.createProperty(it) }
|
||||
}
|
||||
|
||||
var anchor = calculateAnchor(commonParent, commonContainer, allReplaces) ?: return
|
||||
val needBraces = commonContainer !is KtBlockExpression
|
||||
if (!needBraces) {
|
||||
property = commonContainer.addBefore(property, anchor) as KtProperty
|
||||
property = commonContainer.addBefore(property, anchor) as KtDeclaration
|
||||
commonContainer.addBefore(psiFactory.createNewLine(), anchor)
|
||||
}
|
||||
else {
|
||||
@@ -365,6 +378,70 @@ object KotlinIntroduceVariableHandler : KotlinIntroduceHandlerBase() {
|
||||
CommonRefactoringUtil.showErrorHint(project, editor, message, INTRODUCE_VARIABLE, HelpID.INTRODUCE_VARIABLE)
|
||||
}
|
||||
|
||||
private fun KtExpression.chooseApplicableComponentFunctions(haveOccurrencesToReplace: Boolean): List<FunctionDescriptor>? {
|
||||
if (haveOccurrencesToReplace) return emptyList()
|
||||
|
||||
val functions = getApplicableComponentFunctions(this)
|
||||
if (functions.size <= 1) return emptyList()
|
||||
|
||||
return functions
|
||||
}
|
||||
|
||||
private fun executeMultiDeclarationTemplate(
|
||||
project: Project,
|
||||
editor: Editor,
|
||||
declaration: KtMultiDeclaration,
|
||||
suggestedNames: List<Collection<String>>) {
|
||||
StartMarkAction.canStart(project)?.let { return }
|
||||
|
||||
val builder = TemplateBuilderImpl(declaration)
|
||||
for ((index, entry) in declaration.entries.withIndex()) {
|
||||
val templateExpression = object : Expression() {
|
||||
private val lookupItems = suggestedNames[index].map { LookupElementBuilder.create(it) }.toTypedArray()
|
||||
|
||||
override fun calculateQuickResult(context: ExpressionContext?) = TextResult(suggestedNames[index].first())
|
||||
|
||||
override fun calculateResult(context: ExpressionContext?) = calculateQuickResult(context)
|
||||
|
||||
override fun calculateLookupItems(context: ExpressionContext?) = lookupItems
|
||||
}
|
||||
builder.replaceElement(entry, templateExpression)
|
||||
}
|
||||
|
||||
val startMarkAction = StartMarkAction.start(editor, project, INTRODUCE_VARIABLE)
|
||||
editor.caretModel.moveToOffset(declaration.startOffset)
|
||||
|
||||
project.executeWriteCommand(INTRODUCE_VARIABLE) {
|
||||
TemplateManager.getInstance(project).startTemplate(
|
||||
editor,
|
||||
builder.buildInlineTemplate(),
|
||||
object: TemplateEditingAdapter() {
|
||||
private fun finishMarkAction() {
|
||||
FinishMarkAction.finish(project, editor, startMarkAction)
|
||||
}
|
||||
|
||||
override fun templateFinished(template: Template?, brokenOff: Boolean) {
|
||||
finishMarkAction()
|
||||
}
|
||||
|
||||
override fun templateCancelled(template: Template?) {
|
||||
finishMarkAction()
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun suggestNamesForComponent(descriptor: FunctionDescriptor, project: Project, validator: (String) -> Boolean): Set<String> {
|
||||
return LinkedHashSet<String>().apply {
|
||||
descriptor.returnType?.let { addAll(KotlinNameSuggester.suggestNamesByType(it, validator)) }
|
||||
|
||||
val componentName = (DescriptorToSourceUtilsIde.getAnyDeclaration(project, descriptor) as? PsiNamedElement)?.name
|
||||
?: descriptor.name.asString()
|
||||
add(KotlinNameSuggester.suggestNameByName(componentName, validator))
|
||||
}
|
||||
}
|
||||
|
||||
fun doRefactoring(
|
||||
project: Project,
|
||||
editor: Editor?,
|
||||
@@ -374,7 +451,7 @@ object KotlinIntroduceVariableHandler : KotlinIntroduceHandlerBase() {
|
||||
resolutionFacade: ResolutionFacade,
|
||||
bindingContext: BindingContext,
|
||||
occurrencesToReplace: List<KtExpression>?,
|
||||
onNonInteractiveFinish: ((KtProperty) -> Unit)?
|
||||
onNonInteractiveFinish: ((KtDeclaration) -> Unit)?
|
||||
) {
|
||||
val substringInfo = expression.extractableSubstringInfo
|
||||
val physicalExpression = expression.substringContextOrThis
|
||||
@@ -440,19 +517,27 @@ object KotlinIntroduceVariableHandler : KotlinIntroduceHandlerBase() {
|
||||
commonContainer = container
|
||||
}
|
||||
|
||||
val componentFunctions = physicalExpression.chooseApplicableComponentFunctions(replaceOccurrence) ?: return@Pass
|
||||
|
||||
val validator = NewDeclarationNameValidator(
|
||||
commonContainer,
|
||||
calculateAnchor(commonParent, commonContainer, allReplaces),
|
||||
NewDeclarationNameValidator.Target.VARIABLES
|
||||
)
|
||||
val suggestedNames = KotlinNameSuggester.suggestNamesByExpressionAndType(expression,
|
||||
substringInfo?.type,
|
||||
bindingContext,
|
||||
validator,
|
||||
"value")
|
||||
val suggestedNames = if (componentFunctions.isNotEmpty()) {
|
||||
val collectingValidator = CollectingNameValidator(filter = validator)
|
||||
componentFunctions.map { suggestNamesForComponent(it, project, collectingValidator) }
|
||||
}
|
||||
else {
|
||||
KotlinNameSuggester.suggestNamesByExpressionAndType(expression,
|
||||
substringInfo?.type,
|
||||
bindingContext,
|
||||
validator,
|
||||
"value").singletonList()
|
||||
}
|
||||
val introduceVariableContext = IntroduceVariableContext(
|
||||
expression, suggestedNames.iterator().next(), allReplaces, commonContainer, commonParent,
|
||||
replaceOccurrence, noTypeInference, expressionType, bindingContext, resolutionFacade
|
||||
expression, suggestedNames, allReplaces, commonContainer, commonParent,
|
||||
replaceOccurrence, noTypeInference, expressionType, componentFunctions, bindingContext, resolutionFacade
|
||||
)
|
||||
project.executeCommand(INTRODUCE_VARIABLE, null) {
|
||||
runWriteAction { introduceVariableContext.runRefactoring() }
|
||||
@@ -471,18 +556,29 @@ object KotlinIntroduceVariableHandler : KotlinIntroduceHandlerBase() {
|
||||
|
||||
PsiDocumentManager.getInstance(project).commitDocument(editor.document)
|
||||
PsiDocumentManager.getInstance(project).doPostponedOperationsAndUnblockDocument(editor.document)
|
||||
KotlinVariableInplaceIntroducer(
|
||||
property,
|
||||
introduceVariableContext.reference,
|
||||
introduceVariableContext.references.toTypedArray(),
|
||||
suggestedNames,
|
||||
/*todo*/ false,
|
||||
/*todo*/ false,
|
||||
expressionType,
|
||||
noTypeInference,
|
||||
project,
|
||||
editor
|
||||
).startInplaceIntroduceTemplate()
|
||||
|
||||
when (property) {
|
||||
is KtProperty -> {
|
||||
KotlinVariableInplaceIntroducer(
|
||||
property,
|
||||
introduceVariableContext.reference,
|
||||
introduceVariableContext.references.toTypedArray(),
|
||||
suggestedNames.single(),
|
||||
/*todo*/ false,
|
||||
/*todo*/ false,
|
||||
expressionType,
|
||||
noTypeInference,
|
||||
project,
|
||||
editor
|
||||
).startInplaceIntroduceTemplate()
|
||||
}
|
||||
|
||||
is KtMultiDeclaration -> {
|
||||
executeMultiDeclarationTemplate(project, editor, property, suggestedNames)
|
||||
}
|
||||
|
||||
else -> throw AssertionError("Unexpected declaration: ${property.getElementTextWithContext()}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -568,7 +664,7 @@ object KotlinIntroduceVariableHandler : KotlinIntroduceHandlerBase() {
|
||||
editor: Editor?,
|
||||
expressionToExtract: KtExpression?,
|
||||
occurrencesToReplace: List<KtExpression>?,
|
||||
onNonInteractiveFinish: ((KtProperty) -> Unit)?
|
||||
onNonInteractiveFinish: ((KtDeclaration) -> Unit)?
|
||||
) {
|
||||
val expression = expressionToExtract?.let { KtPsiUtil.safeDeparenthesize(it) }
|
||||
?: return showErrorHint(project, editor, KotlinRefactoringBundle.message("cannot.refactor.no.expression"))
|
||||
|
||||
+43
@@ -0,0 +1,43 @@
|
||||
/*
|
||||
* Copyright 2010-2015 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.refactoring.introduce.introduceVariable
|
||||
|
||||
import org.jetbrains.kotlin.descriptors.FunctionDescriptor
|
||||
import org.jetbrains.kotlin.idea.analysis.analyzeInContext
|
||||
import org.jetbrains.kotlin.idea.caches.resolve.getResolutionFacade
|
||||
import org.jetbrains.kotlin.idea.util.getResolutionScope
|
||||
import org.jetbrains.kotlin.psi.KtExpression
|
||||
import org.jetbrains.kotlin.psi.KtPsiFactory
|
||||
import org.jetbrains.kotlin.psi.createExpressionByPattern
|
||||
import org.jetbrains.kotlin.resolve.calls.callUtil.getResolvedCall
|
||||
import org.jetbrains.kotlin.util.isValidOperator
|
||||
|
||||
fun getApplicableComponentFunctions(expression: KtExpression): List<FunctionDescriptor> {
|
||||
val facade = expression.getResolutionFacade()
|
||||
val scope = expression.getResolutionScope(facade.analyze(expression), facade)
|
||||
|
||||
val psiFactory = KtPsiFactory(expression)
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
return sequence(1) { it + 1 }
|
||||
.map {
|
||||
val componentCallExpr = psiFactory.createExpressionByPattern("$0.$1", expression, "component$it()")
|
||||
val newContext = componentCallExpr.analyzeInContext(scope, expression)
|
||||
componentCallExpr.getResolvedCall(newContext)?.resultingDescriptor as? FunctionDescriptor
|
||||
}
|
||||
.takeWhile { it != null && it.isValidOperator() }
|
||||
.toList() as List<FunctionDescriptor>
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
class A
|
||||
|
||||
fun A.component1() = 1
|
||||
fun A.component2() = 2
|
||||
|
||||
fun test() {
|
||||
<selection>A()</selection>
|
||||
}
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
class A
|
||||
|
||||
fun A.component1() = 1
|
||||
fun A.component2() = 2
|
||||
|
||||
fun test() {
|
||||
val a = A()
|
||||
}
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
class A
|
||||
|
||||
operator fun A.component1() = 1
|
||||
|
||||
fun test() {
|
||||
<selection>A()</selection>
|
||||
}
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
class A
|
||||
|
||||
operator fun A.component1() = 1
|
||||
|
||||
fun test() {
|
||||
val a = A()
|
||||
}
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
class A
|
||||
|
||||
operator fun A.component1() = 1
|
||||
operator fun A.component2() = 2
|
||||
|
||||
class B {
|
||||
operator fun A.component3() = 3
|
||||
}
|
||||
|
||||
fun test() {
|
||||
<selection>A()</selection>
|
||||
}
|
||||
Vendored
+12
@@ -0,0 +1,12 @@
|
||||
class A
|
||||
|
||||
operator fun A.component1() = 1
|
||||
operator fun A.component2() = 2
|
||||
|
||||
class B {
|
||||
operator fun A.component3() = 3
|
||||
}
|
||||
|
||||
fun test() {
|
||||
val (i, i1) = A()
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
// WITH_RUNTIME
|
||||
fun test() {
|
||||
<selection>1 to 2</selection>
|
||||
}
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
// WITH_RUNTIME
|
||||
fun test() {
|
||||
val (i, i1) = 1 to 2
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
// WITH_RUNTIME
|
||||
fun test() {
|
||||
(<selection>1 to 2</selection>).first
|
||||
}
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
// WITH_RUNTIME
|
||||
fun test() {
|
||||
val pair = 1 to 2
|
||||
pair.first
|
||||
}
|
||||
+39
@@ -508,6 +508,45 @@ public class ExtractionTestGenerated extends AbstractExtractionTest {
|
||||
}
|
||||
}
|
||||
|
||||
@TestMetadata("idea/testData/refactoring/introduceVariable/multiDeclarations")
|
||||
@TestDataPath("$PROJECT_ROOT")
|
||||
@RunWith(JUnit3RunnerWithInners.class)
|
||||
public static class MultiDeclarations extends AbstractExtractionTest {
|
||||
public void testAllFilesPresentInMultiDeclarations() throws Exception {
|
||||
KotlinTestUtils.assertAllTestsPresentByMetadata(this.getClass(), new File("idea/testData/refactoring/introduceVariable/multiDeclarations"), Pattern.compile("^(.+)\\.kt$"), true);
|
||||
}
|
||||
|
||||
@TestMetadata("notOperators.kt")
|
||||
public void testNotOperators() throws Exception {
|
||||
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/refactoring/introduceVariable/multiDeclarations/notOperators.kt");
|
||||
doIntroduceVariableTest(fileName);
|
||||
}
|
||||
|
||||
@TestMetadata("singleComponent.kt")
|
||||
public void testSingleComponent() throws Exception {
|
||||
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/refactoring/introduceVariable/multiDeclarations/singleComponent.kt");
|
||||
doIntroduceVariableTest(fileName);
|
||||
}
|
||||
|
||||
@TestMetadata("unresolvedComponent3.kt")
|
||||
public void testUnresolvedComponent3() throws Exception {
|
||||
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/refactoring/introduceVariable/multiDeclarations/unresolvedComponent3.kt");
|
||||
doIntroduceVariableTest(fileName);
|
||||
}
|
||||
|
||||
@TestMetadata("unusedExpr.kt")
|
||||
public void testUnusedExpr() throws Exception {
|
||||
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/refactoring/introduceVariable/multiDeclarations/unusedExpr.kt");
|
||||
doIntroduceVariableTest(fileName);
|
||||
}
|
||||
|
||||
@TestMetadata("usedExpr.kt")
|
||||
public void testUsedExpr() throws Exception {
|
||||
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/refactoring/introduceVariable/multiDeclarations/usedExpr.kt");
|
||||
doIntroduceVariableTest(fileName);
|
||||
}
|
||||
}
|
||||
|
||||
@TestMetadata("idea/testData/refactoring/introduceVariable/stringTemplates")
|
||||
@TestDataPath("$PROJECT_ROOT")
|
||||
@RunWith(JUnit3RunnerWithInners.class)
|
||||
|
||||
Reference in New Issue
Block a user