Skip to content

Commit 0c995d0

Browse files
committed
Merge pull request JetBrains#808 from mcgee/KT-10196
KT-10196: Suggest to replace 'substring' calls by take/drop/dropLast calls when possible
2 parents 63dd0fc + b2e98e9 commit 0c995d0

File tree

52 files changed

+612
-0
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

52 files changed

+612
-0
lines changed
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
s.<spot>dropLast(5)</spot>
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
s.<spot>substring(0, s.length - 5)</spot>
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
<html>
2+
<body>
3+
This intention replaces call like <code>s.substring(0, s.length - x)</code> with <code>s.dropLast(x)</code> call.
4+
</body>
5+
</html>
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
s.<spot>substringAfter('x')</spot>
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
s.<spot>substring(s.indexOf('x'))</spot>
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
<html>
2+
<body>
3+
This intention replaces call like <code>s.substring(s.indexOf(x))</code> with <code>s.substringAfter(x)</code> call.
4+
</body>
5+
</html>
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
s.<spot>substringBefore('x')</spot>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
s.<spot>substring(0, s.indexOf('x'))</spot>
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
<html>
2+
<body>
3+
This intention replaces call like <code>s.substring(0, s.indexOf(x))</code> with <code>s.substringBefore(x)</code> call.
4+
</body>
5+
</html>
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
s.<spot>take(10)</spot>
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
s.<spot>substring(0, 10)</spot>
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
<html>
2+
<body>
3+
This intention replaces call like <code>s.substring(0, x)</code> with <code>s.take(x)</code> call.
4+
</body>
5+
</html>

idea/src/META-INF/plugin.xml

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -886,6 +886,26 @@
886886
<category>Kotlin</category>
887887
</intentionAction>
888888

889+
<intentionAction>
890+
<className>org.jetbrains.kotlin.idea.intentions.ReplaceSubstringWithDropLastIntention</className>
891+
<category>Kotlin</category>
892+
</intentionAction>
893+
894+
<intentionAction>
895+
<className>org.jetbrains.kotlin.idea.intentions.ReplaceSubstringWithSubstringAfterIntention</className>
896+
<category>Kotlin</category>
897+
</intentionAction>
898+
899+
<intentionAction>
900+
<className>org.jetbrains.kotlin.idea.intentions.ReplaceSubstringWithSubstringBeforeIntention</className>
901+
<category>Kotlin</category>
902+
</intentionAction>
903+
904+
<intentionAction>
905+
<className>org.jetbrains.kotlin.idea.intentions.ReplaceSubstringWithTakeIntention</className>
906+
<category>Kotlin</category>
907+
</intentionAction>
908+
889909
<intentionAction>
890910
<className>org.jetbrains.kotlin.idea.intentions.RemoveBracesIntention</className>
891911
<category>Kotlin</category>
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
/*
2+
* Copyright 2010-2016 JetBrains s.r.o.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.jetbrains.kotlin.idea.intentions
18+
19+
import com.intellij.openapi.util.TextRange
20+
import org.jetbrains.kotlin.idea.caches.resolve.analyze
21+
import org.jetbrains.kotlin.idea.intentions.branchedTransformations.evaluatesTo
22+
import org.jetbrains.kotlin.idea.intentions.branchedTransformations.isStableVariable
23+
import org.jetbrains.kotlin.psi.*
24+
import org.jetbrains.kotlin.resolve.calls.callUtil.getResolvedCall
25+
import org.jetbrains.kotlin.resolve.constants.evaluate.ConstantExpressionEvaluator
26+
import org.jetbrains.kotlin.resolve.descriptorUtil.fqNameUnsafe
27+
import org.jetbrains.kotlin.resolve.lazy.BodyResolveMode
28+
29+
abstract class ReplaceSubstringIntention(text: String) : SelfTargetingRangeIntention<KtDotQualifiedExpression>(KtDotQualifiedExpression::class.java, text) {
30+
protected abstract fun applicabilityRangeInner(element: KtDotQualifiedExpression): TextRange?
31+
32+
override fun applicabilityRange(element: KtDotQualifiedExpression): TextRange? {
33+
if (element.receiverExpression.isStableVariable() && element.isMethodCall("kotlin.text.substring")) {
34+
return applicabilityRangeInner(element)
35+
}
36+
return null
37+
}
38+
39+
protected fun isIndexOfCall(expression: KtExpression?, expectedReceiver: KtExpression): Boolean {
40+
return expression is KtDotQualifiedExpression
41+
&& expression.isMethodCall("kotlin.text.indexOf")
42+
&& expression.receiverExpression.evaluatesTo(expectedReceiver)
43+
&& expression.callExpression!!.valueArguments.size == 1
44+
}
45+
46+
private fun KtDotQualifiedExpression.isMethodCall(fqMethodName: String): Boolean {
47+
val resolvedCall = toResolvedCall(BodyResolveMode.PARTIAL) ?: return false
48+
return resolvedCall.resultingDescriptor.fqNameUnsafe.asString() == fqMethodName
49+
}
50+
51+
protected fun KtDotQualifiedExpression.isFirstArgumentZero(): Boolean {
52+
val bindingContext = analyze()
53+
val resolvedCall = callExpression.getResolvedCall(bindingContext) ?: return false
54+
val expression = resolvedCall.call.valueArguments[0].getArgumentExpression() as? KtConstantExpression ?: return false
55+
56+
val constant = ConstantExpressionEvaluator.getConstant(expression, bindingContext) ?: return false
57+
val constantType = bindingContext.getType(expression) ?: return false
58+
return constant.getValue(constantType) == 0
59+
}
60+
61+
protected fun getTextRange(element: KtDotQualifiedExpression): TextRange? {
62+
return element.callExpression?.textRange
63+
}
64+
65+
protected fun KtDotQualifiedExpression.replaceWith(pattern: String, argument: KtExpression) {
66+
val psiFactory = KtPsiFactory(this)
67+
replace(psiFactory.createExpressionByPattern(pattern, receiverExpression, argument))
68+
}
69+
}
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
/*
2+
* Copyright 2010-2016 JetBrains s.r.o.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.jetbrains.kotlin.idea.intentions
18+
19+
import com.intellij.openapi.editor.Editor
20+
import com.intellij.openapi.util.TextRange
21+
import org.jetbrains.kotlin.idea.intentions.branchedTransformations.evaluatesTo
22+
import org.jetbrains.kotlin.lexer.KtTokens
23+
import org.jetbrains.kotlin.psi.*
24+
25+
class ReplaceSubstringWithDropLastIntention : ReplaceSubstringIntention("Replace 'substring' call with 'dropLast' call") {
26+
override fun applicabilityRangeInner(element: KtDotQualifiedExpression): TextRange? {
27+
val arguments = element.callExpression?.valueArguments ?: return null
28+
if (arguments.size != 2 || !element.isFirstArgumentZero()) return null
29+
30+
val secondArgumentExpression = arguments[1].getArgumentExpression()
31+
32+
if (secondArgumentExpression !is KtBinaryExpression) return null
33+
if (secondArgumentExpression.operationReference.getReferencedNameElementType() != KtTokens.MINUS) return null
34+
if (isLengthAccess(secondArgumentExpression.left, element.receiverExpression)) {
35+
return getTextRange(element)
36+
}
37+
38+
return null
39+
}
40+
41+
override fun applyTo(element: KtDotQualifiedExpression, editor: Editor?) {
42+
val argument = element.callExpression!!.valueArguments[1].getArgumentExpression()!!
43+
val rightExpression = (argument as KtBinaryExpression).right!!
44+
45+
element.replaceWith("$0.dropLast($1)", rightExpression)
46+
}
47+
48+
private fun isLengthAccess(expression: KtExpression?, expectedReceiver: KtExpression): Boolean {
49+
return expression is KtDotQualifiedExpression
50+
&& expression.selectorExpression.let { it is KtNameReferenceExpression && it.getReferencedName() == "length" }
51+
&& expression.receiverExpression.evaluatesTo(expectedReceiver)
52+
}
53+
}
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
/*
2+
* Copyright 2010-2016 JetBrains s.r.o.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.jetbrains.kotlin.idea.intentions
18+
19+
import com.intellij.openapi.editor.Editor
20+
import com.intellij.openapi.util.TextRange
21+
import org.jetbrains.kotlin.idea.intentions.branchedTransformations.evaluatesTo
22+
import org.jetbrains.kotlin.psi.*
23+
import org.jetbrains.kotlin.resolve.descriptorUtil.fqNameUnsafe
24+
import org.jetbrains.kotlin.resolve.lazy.BodyResolveMode
25+
26+
class ReplaceSubstringWithSubstringAfterIntention : ReplaceSubstringIntention("Replace 'substring' call with 'substringAfter' call") {
27+
override fun applicabilityRangeInner(element: KtDotQualifiedExpression): TextRange? {
28+
val arguments = element.callExpression?.valueArguments ?: return null
29+
30+
if (arguments.size == 1 && isIndexOfCall(arguments[0].getArgumentExpression(), element.receiverExpression)) {
31+
return getTextRange(element)
32+
}
33+
34+
return null
35+
}
36+
37+
override fun applyTo(element: KtDotQualifiedExpression, editor: Editor?) {
38+
element.replaceWith(
39+
"$0.substringAfter($1)",
40+
(element.getArgumentExpression(0) as KtDotQualifiedExpression).getArgumentExpression(0))
41+
}
42+
}
43+
44+
class ReplaceSubstringWithSubstringBeforeIntention : ReplaceSubstringIntention("Replace 'substring' call with 'substringBefore' call") {
45+
override fun applicabilityRangeInner(element: KtDotQualifiedExpression): TextRange? {
46+
val arguments = element.callExpression?.valueArguments ?: return null
47+
48+
if (arguments.size == 2
49+
&& element.isFirstArgumentZero()
50+
&& isIndexOfCall(arguments[1].getArgumentExpression(), element.receiverExpression)) {
51+
return getTextRange(element)
52+
}
53+
54+
return null
55+
}
56+
57+
override fun applyTo(element: KtDotQualifiedExpression, editor: Editor?) {
58+
element.replaceWith(
59+
"$0.substringBefore($1)",
60+
(element.getArgumentExpression(1) as KtDotQualifiedExpression).getArgumentExpression(0))
61+
}
62+
}
63+
64+
private fun KtDotQualifiedExpression.getArgumentExpression(index: Int): KtExpression {
65+
return callExpression!!.valueArguments[index].getArgumentExpression()!!
66+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
/*
2+
* Copyright 2010-2016 JetBrains s.r.o.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.jetbrains.kotlin.idea.intentions
18+
19+
import com.intellij.openapi.editor.Editor
20+
import com.intellij.openapi.util.TextRange
21+
import org.jetbrains.kotlin.psi.KtDotQualifiedExpression
22+
import org.jetbrains.kotlin.psi.KtPsiFactory
23+
import org.jetbrains.kotlin.psi.createExpressionByPattern
24+
25+
class ReplaceSubstringWithTakeIntention : ReplaceSubstringIntention("Replace 'substring' call with 'take' call") {
26+
override fun applicabilityRangeInner(element: KtDotQualifiedExpression): TextRange? {
27+
val arguments = element.callExpression?.valueArguments ?: return null
28+
if (arguments.size == 2 && element.isFirstArgumentZero()) {
29+
return getTextRange(element)
30+
}
31+
return null
32+
}
33+
34+
override fun applyTo(element: KtDotQualifiedExpression, editor: Editor?) {
35+
val argument = element.callExpression!!.valueArguments[1].getArgumentExpression()!!
36+
element.replaceWith("$0.take($1)", argument)
37+
}
38+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
org.jetbrains.kotlin.idea.intentions.ReplaceSubstringWithDropLastIntention
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
// WITH_RUNTIME
2+
3+
class A(val x: String)
4+
5+
fun foo(a: A) {
6+
a.x.substring<caret>(0, a.x.length - 5)
7+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
// WITH_RUNTIME
2+
3+
class A(val x: String)
4+
5+
fun foo(a: A) {
6+
a.x.dropLast(5)
7+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
// WITH_RUNTIME
2+
// IS_APPLICABLE: false
3+
4+
class A() {
5+
fun bar(): String = null!!
6+
}
7+
8+
fun foo(a: A) {
9+
a.bar().substring<caret>(0, a.bar().length - 5)
10+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
// IS_APPLICABLE: false
2+
// WITH_RUNTIME
3+
4+
fun foo(s: String) {
5+
s.substring<caret>(3, s.length - 5)
6+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
// WITH_RUNTIME
2+
3+
fun foo(s: String) {
4+
s.substring<caret>(0, s.length - 5)
5+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
// WITH_RUNTIME
2+
3+
fun foo(s: String) {
4+
s.dropLast(5)
5+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
// WITH_RUNTIME
2+
3+
fun foo(s: String) {
4+
s.substring<caret>(0, s.length - 5);
5+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
// WITH_RUNTIME
2+
3+
fun foo(s: String) {
4+
s.dropLast(5);
5+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
org.jetbrains.kotlin.idea.intentions.ReplaceSubstringWithSubstringAfterIntention
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
// WITH_RUNTIME
2+
3+
class A(val x: String)
4+
5+
fun foo(a: A) {
6+
a.x.substring<caret>(a.x.indexOf('x'))
7+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
// WITH_RUNTIME
2+
3+
class A(val x: String)
4+
5+
fun foo(a: A) {
6+
a.x.substringAfter('x')
7+
}

0 commit comments

Comments
 (0)