Debugger: implement Smart Step Into

#KT-4639 Fixed
This commit is contained in:
Natalia Ukhorskaya
2014-03-21 11:13:19 +04:00
parent 3bc35adf09
commit e8d2ba197e
32 changed files with 747 additions and 0 deletions
@@ -0,0 +1,12 @@
<root>
<item
name='com.intellij.debugger.actions.JvmSmartStepIntoHandler java.util.List&lt;com.intellij.debugger.actions.SmartStepTarget&gt; findSmartStepTargets(com.intellij.debugger.SourcePosition)'>
<annotation name='kotlin.jvm.KotlinSignature'>
<val name="value" val="&quot;fun findSmartStepTargets(position: SourcePosition): List&lt;SmartStepTarget&gt;&quot;"/>
</annotation>
</item>
<item
name='com.intellij.debugger.actions.JvmSmartStepIntoHandler java.util.List&lt;com.intellij.debugger.actions.SmartStepTarget&gt; findSmartStepTargets(com.intellij.debugger.SourcePosition) 0'>
<annotation name='org.jetbrains.annotations.NotNull'/>
</item>
</root>
@@ -8,4 +8,10 @@
<item name='com.intellij.util.QueryFactory com.intellij.util.Query&lt;Result&gt; createUniqueResultsQuery(Parameters)'>
<annotation name='org.jetbrains.annotations.NotNull'/>
</item>
<item name='com.intellij.util.Range T getFrom()'>
<annotation name='org.jetbrains.annotations.NotNull'/>
</item>
<item name='com.intellij.util.Range T getTo()'>
<annotation name='org.jetbrains.annotations.NotNull'/>
</item>
</root>
@@ -99,6 +99,7 @@ import org.jetbrains.jet.plugin.refactoring.move.AbstractJetMoveTest
import org.jetbrains.jet.cfg.AbstractDataFlowTest
import org.jetbrains.jet.plugin.libraries.AbstractDecompiledTextTest
import org.jetbrains.jet.plugin.imports.AbstractOptimizeImportsTest
import org.jetbrains.jet.plugin.debugger.AbstractSmartStepIntoTest
fun main(args: Array<String>) {
System.setProperty("java.awt.headless", "true")
@@ -524,6 +525,10 @@ fun main(args: Array<String>) {
testClass(javaClass<AbstractOptimizeImportsTest>()) {
model("editor/optimizeImports", extension = null, recursive = false)
}
testClass(javaClass<AbstractSmartStepIntoTest>()) {
model("debugger/smartStepInto")
}
}
testGroup("j2k/tests/test", "j2k/tests/testData") {
+1
View File
@@ -299,6 +299,7 @@
id="kotlinJavaSafeDeleteDelegate"
language="jet"
implementationClass="org.jetbrains.jet.plugin.refactoring.safeDelete.KotlinJavaSafeDeleteDelegate"/>
<debugger.jvmSmartStepIntoHandler implementation="org.jetbrains.jet.plugin.debugger.KotlinSmartStepIntoHandler"/>
<debugger.positionManagerFactory implementation="org.jetbrains.jet.plugin.debugger.JetPositionManagerFactory"/>
<codeInsight.implementMethod language="jet" implementationClass="org.jetbrains.jet.plugin.codeInsight.ImplementMethodsHandler"/>
<codeInsight.overrideMethod language="jet" implementationClass="org.jetbrains.jet.plugin.codeInsight.OverrideMethodsHandler"/>
@@ -0,0 +1,190 @@
/*
* Copyright 2010-2014 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.jet.plugin.debugger
import com.intellij.debugger.actions.JvmSmartStepIntoHandler
import com.intellij.debugger.SourcePosition
import com.intellij.debugger.actions.SmartStepTarget
import java.util.Collections
import com.intellij.openapi.fileEditor.FileDocumentManager
import com.intellij.debugger.actions.MethodSmartStepTarget
import com.intellij.util.containers.OrderedSet
import com.intellij.util.Range
import org.jetbrains.jet.lang.resolve.BindingContext
import org.jetbrains.jet.plugin.project.AnalyzerFacadeWithCache
import org.jetbrains.jet.asJava.LightClassUtil
import org.jetbrains.jet.lang.resolve.BindingContextUtils
import org.jetbrains.jet.lang.descriptors.CallableMemberDescriptor
import com.intellij.psi.PsiElement
import com.intellij.util.text.CharArrayUtil
import org.jetbrains.jet.lang.psi.*
import org.jetbrains.jet.lang.descriptors.PropertyDescriptor
public class KotlinSmartStepIntoHandler : JvmSmartStepIntoHandler() {
override fun isAvailable(position: SourcePosition?) = position?.getFile() is JetFile
override fun findSmartStepTargets(position: SourcePosition): List<SmartStepTarget> {
if (position.getLine() < 0) return Collections.emptyList()
val file = position.getFile()
val vFile = file.getVirtualFile()
if (vFile == null) return Collections.emptyList()
val doc = FileDocumentManager.getInstance()?.getDocument(vFile)
if (doc == null) return Collections.emptyList()
val line = position.getLine()
if (line >= doc.getLineCount()) return Collections.emptyList()
val lineStartOffset = doc.getLineStartOffset(line)
val offsetWithoutTab = CharArrayUtil.shiftForward(doc.getCharsSequence(), lineStartOffset, " \t")
val elementAtOffset = file.findElementAt(offsetWithoutTab)
if (elementAtOffset == null) return Collections.emptyList()
val element = getTopmostElementAtOffset(elementAtOffset, lineStartOffset)
if (element !is JetElement) return Collections.emptyList()
val elementTextRange = element.getTextRange()
if (elementTextRange == null) return Collections.emptyList()
val lines = Range<Int>(doc.getLineNumber(elementTextRange.getStartOffset()), doc.getLineNumber(elementTextRange.getEndOffset()))
val bindingContext = AnalyzerFacadeWithCache.getContextForElement(element)
val result = OrderedSet<SmartStepTarget>()
// TODO support class initializers, local functions, delegated properties with specified type, setter for properties
element.accept(object: JetTreeVisitorVoid() {
override fun visitFunctionLiteralExpression(expression: JetFunctionLiteralExpression) {
// skip calls in function literals
}
override fun visitObjectLiteralExpression(expression: JetObjectLiteralExpression) {
// skip calls in object declarations
}
override fun visitIfExpression(expression: JetIfExpression) {
expression.getCondition()?.accept(this)
}
override fun visitWhileExpression(expression: JetWhileExpression) {
expression.getCondition()?.accept(this)
}
override fun visitDoWhileExpression(expression: JetDoWhileExpression) {
expression.getCondition()?.accept(this)
}
override fun visitForExpression(expression: JetForExpression) {
expression.getLoopRange()?.accept(this)
}
override fun visitWhenExpression(expression: JetWhenExpression) {
expression.getSubjectExpression()?.accept(this)
}
override fun visitArrayAccessExpression(expression: JetArrayAccessExpression) {
recordFunction(expression)
super.visitArrayAccessExpression(expression)
}
override fun visitUnaryExpression(expression: JetUnaryExpression) {
recordFunction(expression.getOperationReference())
super.visitUnaryExpression(expression)
}
override fun visitBinaryExpression(expression: JetBinaryExpression) {
recordFunction(expression.getOperationReference())
super.visitBinaryExpression(expression)
}
override fun visitCallExpression(expression: JetCallExpression) {
val calleeExpression = expression.getCalleeExpression()
if (calleeExpression != null) {
recordFunction(calleeExpression)
}
super.visitCallExpression(expression)
}
override fun visitSimpleNameExpression(expression: JetSimpleNameExpression) {
val resolvedCall = bindingContext.get(BindingContext.RESOLVED_CALL, expression)
if (resolvedCall != null) {
val propertyDescriptor = resolvedCall.getResultingDescriptor()
if (propertyDescriptor is PropertyDescriptor) {
val getterDescriptor = propertyDescriptor.getGetter()
if (getterDescriptor != null && !getterDescriptor.isDefault()) {
val delegatedResolvedCall = bindingContext.get(BindingContext.DELEGATED_PROPERTY_RESOLVED_CALL, getterDescriptor)
if (delegatedResolvedCall == null) {
val getter = BindingContextUtils.callableDescriptorToDeclaration(bindingContext, getterDescriptor)
if (getter is JetPropertyAccessor && (getter.getBodyExpression() != null || getter.getEqualsToken() != null)) {
val psiMethod = LightClassUtil.getLightClassAccessorMethod(getter)
if (psiMethod != null) {
result.add(MethodSmartStepTarget(psiMethod, null, expression, false, lines))
}
}
}
else {
val delegatedPropertyGetterDescriptor = delegatedResolvedCall.getResultingDescriptor()
if (delegatedPropertyGetterDescriptor is CallableMemberDescriptor) {
val function = BindingContextUtils.callableDescriptorToDeclaration(bindingContext, delegatedPropertyGetterDescriptor)
if (function is JetNamedFunction) {
val psiMethod = LightClassUtil.getLightClassMethod(function)
if (psiMethod != null) {
result.add(MethodSmartStepTarget(psiMethod, "${propertyDescriptor.getName()}.", expression, false, lines))
}
}
}
}
}
}
}
super.visitSimpleNameExpression(expression)
}
private fun recordFunction(expression: JetExpression) {
val resolvedCall = bindingContext.get(BindingContext.RESOLVED_CALL, expression)
if (resolvedCall == null) return
val descriptor = resolvedCall.getResultingDescriptor()
if (descriptor is CallableMemberDescriptor) {
val function = BindingContextUtils.callableDescriptorToDeclaration(bindingContext, descriptor)
if (function is JetNamedFunction) {
val psiMethod = LightClassUtil.getLightClassMethod(function)
if (psiMethod != null) {
result.add(MethodSmartStepTarget(method = psiMethod, label = null,
highlightElement = expression,
needBreakpointRequest = false, lines = lines))
}
}
}
}
}, null)
return result
}
private fun getTopmostElementAtOffset(element: PsiElement, offset: Int): PsiElement? {
var resultElement: PsiElement? = element
while (resultElement?.getParent()?.getTextRange() != null &&
resultElement?.getParent()?.getTextRange()!!.getStartOffset() >= offset) {
resultElement = resultElement!!.getParent()
}
return resultElement
}
}
@@ -0,0 +1,9 @@
fun foo() {
[Ann() Ann] val a = bar()<caret>
}
annotation class Ann
fun bar() = 1
// EXISTS: bar()
@@ -0,0 +1,10 @@
fun foo() {
val a = A()
<caret>a[1]
}
class A {
fun get(i: Int) = 1
}
// EXISTS: get(int)
@@ -0,0 +1,13 @@
fun foo() {
A().getB().f1()<caret>
}
class A {
fun getB() = B()
}
class B {
fun f1() {}
}
// EXISTS: getB(), f1()
@@ -0,0 +1,11 @@
class A {
fun plus(a: A) {}
}
fun foo() {
f1() + A() + A()<caret>
}
fun f1() = A()
// EXISTS: plus(A), f1()
@@ -0,0 +1,11 @@
fun foo() {
a<caret>
}
val a by Delegate()
class Delegate {
fun get(t: Any?, p: PropertyMetadata) = 1
}
// EXISTS: a.get(Object\, PropertyMetadata)
@@ -0,0 +1,10 @@
fun foo() {
<caret>do {
f2()
} while (f1())
}
fun f1() = true
fun f2() {}
// EXISTS: f1()
@@ -0,0 +1,12 @@
fun foo() {
val a = A()
a.f1(f2())<caret>
}
class A {
fun f1(): Int = 1
}
fun f2() {}
// EXISTS: f1(), f2()
@@ -0,0 +1,12 @@
fun foo() {
val a = A()
f2(a.f1())<caret>
}
class A {
fun f1() = 1
}
fun f2(i: Int) {}
// EXISTS: f1(), f2(int)
@@ -0,0 +1,3 @@
fun foo() {
val a = 1<caret>
}
@@ -0,0 +1,11 @@
fun foo() {
val a = 1
<caret>for (a in f1()) {
f2()
}
}
fun f1() = 1..2
fun f2() {}
// EXISTS: f1()
@@ -0,0 +1,10 @@
fun foo() {
<caret>f1() {
f2()
}
}
fun f1(f: () -> Unit) {}
fun f2() {}
// EXISTS: f1(Function0<? extends Unit>)
@@ -0,0 +1,10 @@
fun foo() {
<caret>if (f1()) {
f2()
}
}
fun f1() = true
fun f2() {}
// EXISTS: f1()
@@ -0,0 +1,12 @@
fun foo() {
val a = A()
f2(a f1 1)<caret>
}
class A {
fun f1(i: Int) = 1
}
fun f2(i: Int) {}
// EXISTS: f1(int), f2(int)
@@ -0,0 +1,10 @@
fun foo() {
val a = A()
a()<caret>
}
class A {
fun invoke() {}
}
// EXISTS: invoke()
@@ -0,0 +1,12 @@
fun foo() {
<caret>f1(
f2(),
f3()
)
}
fun f1(vararg i: Int) {}
fun f2() = 1
fun f3() = 1
// EXISTS: f1(int...), f2(), f3()
@@ -0,0 +1,15 @@
fun foo() {
<caret>A()
.getB()
.f1()
}
class A {
fun getB() = B()
}
class B {
fun f1() {}
}
// EXISTS: getB(), f1()
@@ -0,0 +1,9 @@
fun foo() {
<caret>val a = object {
fun f() {
f2()
}
}
}
fun f2() {}
@@ -0,0 +1,8 @@
fun foo() {
f1(f2())<caret>
}
fun f1(i: Int) = 1
fun f2() {}
// EXISTS: f1(int), f2()
@@ -0,0 +1,7 @@
fun foo() {
(bar())<caret>
}
fun bar() {}
// EXISTS: bar()
@@ -0,0 +1,18 @@
fun foo() {
a + b + c + d<caret>
}
val a = 1
val b = 1
get
val c: Int
get() = 1
val d: Int
get() {
return 1
}
// EXISTS: getC(), getD()
@@ -0,0 +1,7 @@
fun foo() {
bar()<caret>
}
fun bar() {}
// EXISTS: bar()
@@ -0,0 +1,8 @@
fun foo() {
<caret>f2("aaa${f1()}")
}
fun f1() = "1"
fun f2(s: String) {}
// EXISTS: f1(), f2(String)
@@ -0,0 +1,9 @@
class A {
fun plus() {}
fun minus() {}
}
fun foo() {
+A() || A()-<caret>
}
// EXISTS: plus(), minus()
@@ -0,0 +1,13 @@
fun foo() {
<caret>when (f1()) {
true -> f2()
else -> {
f2()
}
}
}
fun f1() = true
fun f2() {}
// EXISTS: f1()
@@ -0,0 +1,10 @@
fun foo() {
<caret>while (f1()) {
f2()
}
}
fun f1() = true
fun f2() {}
// EXISTS: f1()
@@ -0,0 +1,109 @@
/*
* Copyright 2010-2014 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.jet.plugin.debugger
import com.intellij.debugger.SourcePosition
import com.intellij.psi.PsiElement
import com.intellij.debugger.actions.SmartStepTarget
import org.jetbrains.jet.plugin.JetLightCodeInsightFixtureTestCase
import com.intellij.testFramework.fixtures.JavaCodeInsightTestFixture
import org.jetbrains.jet.plugin.PluginTestCaseBase
import com.intellij.debugger.actions.MethodSmartStepTarget
import org.jetbrains.jet.InTextDirectivesUtils
import com.intellij.psi.util.PsiFormatUtil
import com.intellij.psi.PsiSubstitutor
import com.intellij.psi.util.PsiFormatUtilBase
abstract class AbstractSmartStepIntoTest : JetLightCodeInsightFixtureTestCase() {
private val fixture: JavaCodeInsightTestFixture
get() = myFixture!!
protected fun doTest(path: String) {
fixture.configureByFile(path)
val offset = fixture.getCaretOffset()
val line = fixture.getDocument(fixture.getFile())!!.getLineNumber(offset)
val position = object: SourcePosition() {
override fun getFile() = fixture.getFile()!!
override fun getElementAt(): PsiElement? = throw UnsupportedOperationException()
override fun getLine() = line
override fun getOffset() = offset
override fun openEditor(requestFocus: Boolean) = fixture.getEditor()
override fun navigate(requestFocus: Boolean) {
}
override fun canNavigate() = false
override fun canNavigateToSource() = false
}
val actual = KotlinSmartStepIntoHandler().findSmartStepTargets(position).map { renderTarget(it) }
val expected = InTextDirectivesUtils.findListWithPrefixes(fixture.getFile()?.getText()!!.replace("\\,", "+++"), "// EXISTS: ").map { it.replace("+++", ",") }
for (actualTargetName in actual) {
assert(expected.contains(actualTargetName), "Unexpected step into target was found: ${actualTargetName}\n${renderTableWithResults(expected, actual)}")
}
for (expectedTargetName in expected) {
assert(actual.contains(expectedTargetName), "Missed step into target: ${expectedTargetName}\n${renderTableWithResults(expected, actual)}")
}
}
private fun renderTableWithResults(expected: List<String>, actual: List<String>): String {
val sb = StringBuilder()
val maxExtStrSize = (expected.maxBy { it.size }?.size ?: 0) + 5
val longerList = if (expected.size < actual.size) actual else expected
val shorterList = if (expected.size < actual.size) expected else actual
for ((i, element) in longerList.withIndices()) {
sb.append(element)
sb.append(" ".repeat(maxExtStrSize - element.size))
if (i < shorterList.size) sb.append(shorterList[i])
sb.append("\n")
}
return sb.toString()
}
private fun renderTarget(target: SmartStepTarget): String {
val sb = StringBuilder()
val label = target.getLabel()
if (label != null) {
sb.append(label)
}
when (target) {
is MethodSmartStepTarget -> {
sb.append(PsiFormatUtil.formatMethod(
target.getMethod(),
PsiSubstitutor.EMPTY,
PsiFormatUtilBase.SHOW_NAME or PsiFormatUtilBase.SHOW_PARAMETERS,
PsiFormatUtilBase.SHOW_TYPE,
999
))
}
else -> {
sb.append("Renderer for ${target.javaClass} should be implemented")
}
}
return sb.toString()
}
override fun getTestDataPath(): String? {
return PluginTestCaseBase.getTestDataPathBase() + "/debugger/smartStepInto"
}
}
@@ -0,0 +1,164 @@
/*
* Copyright 2010-2014 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.jet.plugin.debugger;
import junit.framework.Assert;
import junit.framework.Test;
import junit.framework.TestSuite;
import java.io.File;
import java.util.regex.Pattern;
import org.jetbrains.jet.JetTestUtils;
import org.jetbrains.jet.test.InnerTestClasses;
import org.jetbrains.jet.test.TestMetadata;
import org.jetbrains.jet.plugin.debugger.AbstractSmartStepIntoTest;
/** This class is generated by {@link org.jetbrains.jet.generators.tests.TestsPackage}. DO NOT MODIFY MANUALLY */
@SuppressWarnings("all")
@TestMetadata("idea/testData/debugger/smartStepInto")
public class SmartStepIntoTestGenerated extends AbstractSmartStepIntoTest {
public void testAllFilesPresentInSmartStepInto() throws Exception {
JetTestUtils.assertAllTestsPresentByMetadata(this.getClass(), "org.jetbrains.jet.generators.tests.TestsPackage", new File("idea/testData/debugger/smartStepInto"), Pattern.compile("^(.+)\\.kt$"), true);
}
@TestMetadata("annotation.kt")
public void testAnnotation() throws Exception {
doTest("idea/testData/debugger/smartStepInto/annotation.kt");
}
@TestMetadata("arrayAccess.kt")
public void testArrayAccess() throws Exception {
doTest("idea/testData/debugger/smartStepInto/arrayAccess.kt");
}
@TestMetadata("callChain.kt")
public void testCallChain() throws Exception {
doTest("idea/testData/debugger/smartStepInto/callChain.kt");
}
@TestMetadata("conventionMethod.kt")
public void testConventionMethod() throws Exception {
doTest("idea/testData/debugger/smartStepInto/conventionMethod.kt");
}
@TestMetadata("delegatedPropertyGetter.kt")
public void testDelegatedPropertyGetter() throws Exception {
doTest("idea/testData/debugger/smartStepInto/delegatedPropertyGetter.kt");
}
@TestMetadata("doWhile.kt")
public void testDoWhile() throws Exception {
doTest("idea/testData/debugger/smartStepInto/doWhile.kt");
}
@TestMetadata("dotQualified.kt")
public void testDotQualified() throws Exception {
doTest("idea/testData/debugger/smartStepInto/dotQualified.kt");
}
@TestMetadata("dotQualifiedInParam.kt")
public void testDotQualifiedInParam() throws Exception {
doTest("idea/testData/debugger/smartStepInto/dotQualifiedInParam.kt");
}
@TestMetadata("empty.kt")
public void testEmpty() throws Exception {
doTest("idea/testData/debugger/smartStepInto/empty.kt");
}
@TestMetadata("for.kt")
public void testFor() throws Exception {
doTest("idea/testData/debugger/smartStepInto/for.kt");
}
@TestMetadata("funLiteral.kt")
public void testFunLiteral() throws Exception {
doTest("idea/testData/debugger/smartStepInto/funLiteral.kt");
}
@TestMetadata("if.kt")
public void testIf() throws Exception {
doTest("idea/testData/debugger/smartStepInto/if.kt");
}
@TestMetadata("infixCall.kt")
public void testInfixCall() throws Exception {
doTest("idea/testData/debugger/smartStepInto/infixCall.kt");
}
@TestMetadata("invoke.kt")
public void testInvoke() throws Exception {
doTest("idea/testData/debugger/smartStepInto/invoke.kt");
}
@TestMetadata("multiline.kt")
public void testMultiline() throws Exception {
doTest("idea/testData/debugger/smartStepInto/multiline.kt");
}
@TestMetadata("multilineCallChain.kt")
public void testMultilineCallChain() throws Exception {
doTest("idea/testData/debugger/smartStepInto/multilineCallChain.kt");
}
@TestMetadata("object.kt")
public void testObject() throws Exception {
doTest("idea/testData/debugger/smartStepInto/object.kt");
}
@TestMetadata("param.kt")
public void testParam() throws Exception {
doTest("idea/testData/debugger/smartStepInto/param.kt");
}
@TestMetadata("parantesized.kt")
public void testParantesized() throws Exception {
doTest("idea/testData/debugger/smartStepInto/parantesized.kt");
}
@TestMetadata("propertyGetter.kt")
public void testPropertyGetter() throws Exception {
doTest("idea/testData/debugger/smartStepInto/propertyGetter.kt");
}
@TestMetadata("simple.kt")
public void testSimple() throws Exception {
doTest("idea/testData/debugger/smartStepInto/simple.kt");
}
@TestMetadata("stringTemplate.kt")
public void testStringTemplate() throws Exception {
doTest("idea/testData/debugger/smartStepInto/stringTemplate.kt");
}
@TestMetadata("unary.kt")
public void testUnary() throws Exception {
doTest("idea/testData/debugger/smartStepInto/unary.kt");
}
@TestMetadata("when.kt")
public void testWhen() throws Exception {
doTest("idea/testData/debugger/smartStepInto/when.kt");
}
@TestMetadata("while.kt")
public void testWhile() throws Exception {
doTest("idea/testData/debugger/smartStepInto/while.kt");
}
}