Skip to content

Commit 28c5dde

Browse files
committed
KT-7715 Highlight var's that can be replaced by val's
#KT-7715 Fixed
1 parent e5b5a8d commit 28c5dde

28 files changed

+350
-15
lines changed

compiler/frontend/src/org/jetbrains/kotlin/psi/KtDestructuringDeclaration.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,11 @@ public KtExpression getInitializer() {
5353
return PsiTreeUtil.getNextSiblingOfType(eqNode.getPsi(), KtExpression.class);
5454
}
5555

56+
public boolean isVar() {
57+
return getNode().findChildByType(KtTokens.VAR_KEYWORD) != null;
58+
}
59+
60+
@Override
5661
@Nullable
5762
public PsiElement getValOrVarKeyword() {
5863
return findChildByType(TokenSet.create(VAL_KEYWORD, VAR_KEYWORD));
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
<html>
2+
<body>
3+
This inspection reports mutable local variables (declared with 'var' keyword) that can be made immutable.
4+
</body>
5+
</html>

idea/src/META-INF/plugin.xml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1391,6 +1391,13 @@
13911391
cleanupTool="false"
13921392
level="WEAK WARNING"/>
13931393

1394+
<localInspection implementationClass="org.jetbrains.kotlin.idea.inspections.CanBeValInspection"
1395+
displayName="Local 'var' can be declared as 'val'"
1396+
groupName="Kotlin"
1397+
enabledByDefault="true"
1398+
level="WARNING"
1399+
/>
1400+
13941401
<referenceImporter implementation="org.jetbrains.kotlin.idea.quickfix.KotlinReferenceImporter"/>
13951402

13961403
<fileType.fileViewProviderFactory filetype="KJSM" implementationClass="com.intellij.psi.ClassFileViewProviderFactory"/>
Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
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.inspections
18+
19+
import com.intellij.codeInspection.IntentionWrapper
20+
import com.intellij.codeInspection.ProblemHighlightType
21+
import com.intellij.codeInspection.ProblemsHolder
22+
import com.intellij.psi.PsiElementVisitor
23+
import com.intellij.psi.search.searches.ReferencesSearch
24+
import org.jetbrains.kotlin.cfg.pseudocode.Pseudocode
25+
import org.jetbrains.kotlin.cfg.pseudocode.PseudocodeUtil
26+
import org.jetbrains.kotlin.cfg.pseudocode.containingDeclarationForPseudocode
27+
import org.jetbrains.kotlin.cfg.pseudocode.instructions.Instruction
28+
import org.jetbrains.kotlin.cfg.pseudocode.instructions.InstructionWithNext
29+
import org.jetbrains.kotlin.cfg.pseudocode.instructions.eval.AccessTarget
30+
import org.jetbrains.kotlin.cfg.pseudocode.instructions.eval.WriteValueInstruction
31+
import org.jetbrains.kotlin.idea.caches.resolve.analyze
32+
import org.jetbrains.kotlin.idea.quickfix.ChangeVariableMutabilityFix
33+
import org.jetbrains.kotlin.idea.references.KtSimpleNameReference
34+
import org.jetbrains.kotlin.idea.references.readWriteAccess
35+
import org.jetbrains.kotlin.psi.*
36+
import org.jetbrains.kotlin.resolve.BindingContext
37+
import org.jetbrains.kotlin.resolve.lazy.BodyResolveMode
38+
import java.util.*
39+
40+
class CanBeValInspection : AbstractKotlinInspection() {
41+
override fun buildVisitor(holder: ProblemsHolder, isOnTheFly: Boolean): PsiElementVisitor {
42+
return object: KtVisitorVoid() {
43+
private val pseudocodeCache = HashMap<KtDeclaration, Pseudocode>()
44+
45+
override fun visitDeclaration(declaration: KtDeclaration) {
46+
super.visitDeclaration(declaration)
47+
48+
when (declaration) {
49+
is KtProperty -> {
50+
if (declaration.isVar && declaration.isLocal && canBeVal(declaration, declaration.hasInitializer(), listOf(declaration))) {
51+
reportCanBeVal(declaration)
52+
}
53+
}
54+
55+
is KtDestructuringDeclaration -> {
56+
val entries = declaration.entries
57+
if (declaration.isVar && entries.all { canBeVal(it, true, entries) }) {
58+
reportCanBeVal(declaration)
59+
}
60+
}
61+
}
62+
}
63+
64+
private fun canBeVal(declaration: KtVariableDeclaration, hasInitializer: Boolean, allDeclarations: Collection<KtVariableDeclaration>): Boolean {
65+
if (allDeclarations.all { ReferencesSearch.search(it, it.useScope).none() }) {
66+
// do not report for unused var's (otherwise we'll get it highlighted immediately after typing the declaration
67+
return false
68+
}
69+
70+
if (hasInitializer) {
71+
val hasWriteUsages = ReferencesSearch.search(declaration, declaration.useScope).any {
72+
(it as? KtSimpleNameReference)?.element?.readWriteAccess(useResolveForReadWrite = true)?.isWrite == true
73+
}
74+
return !hasWriteUsages
75+
}
76+
else {
77+
val bindingContext = declaration.analyze(BodyResolveMode.FULL)
78+
val pseudocode = pseudocode(declaration, bindingContext) ?: return false
79+
val descriptor = bindingContext[BindingContext.DECLARATION_TO_DESCRIPTOR, declaration] ?: return false
80+
81+
val writeInstructions = pseudocode.instructionsIncludingDeadCode
82+
.filterIsInstance<WriteValueInstruction>()
83+
.filter { (it.target as? AccessTarget.Call)?.resolvedCall?.resultingDescriptor == descriptor }
84+
.toSet()
85+
if (writeInstructions.isEmpty()) return false // incorrect code - do not report
86+
87+
return writeInstructions.none { canReach(it, writeInstructions) }
88+
}
89+
}
90+
91+
private fun pseudocode(element: KtElement, bindingContext: BindingContext): Pseudocode? {
92+
val declaration = element.containingDeclarationForPseudocode ?: return null
93+
return pseudocodeCache.getOrPut(declaration) { PseudocodeUtil.generatePseudocode(declaration, bindingContext) }
94+
}
95+
96+
private fun canReach(from: Instruction, targets: Set<Instruction>, visited: HashSet<Instruction> = HashSet<Instruction>()): Boolean {
97+
// special algorithm for linear code to avoid too deep recursion
98+
var instruction = from
99+
while (instruction is InstructionWithNext) {
100+
val next = instruction.next ?: return false
101+
if (next in visited) continue
102+
if (next in targets) return true
103+
visited.add(next)
104+
instruction = next
105+
}
106+
107+
for (next in instruction.nextInstructions) {
108+
if (next in visited) continue
109+
if (next in targets) return true
110+
visited.add(next)
111+
if (canReach(next, targets)) return true
112+
}
113+
return false
114+
}
115+
116+
private fun reportCanBeVal(declaration: KtValVarKeywordOwner) {
117+
val problemDescriptor = holder.manager.createProblemDescriptor(
118+
declaration,
119+
declaration.valOrVarKeyword!!,
120+
"Can be declared as 'val'",
121+
ProblemHighlightType.GENERIC_ERROR_OR_WARNING,
122+
isOnTheFly,
123+
IntentionWrapper(ChangeVariableMutabilityFix(declaration, false), declaration.containingFile)
124+
)
125+
holder.registerProblem(problemDescriptor)
126+
}
127+
}
128+
}
129+
}

idea/src/org/jetbrains/kotlin/idea/quickfix/ChangeVariableMutabilityFix.kt

Lines changed: 6 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -24,32 +24,23 @@ import org.jetbrains.kotlin.diagnostics.Diagnostic
2424
import org.jetbrains.kotlin.diagnostics.Errors
2525
import org.jetbrains.kotlin.lexer.KtTokens
2626
import org.jetbrains.kotlin.psi.*
27-
import org.jetbrains.kotlin.psi.psiUtil.getStrictParentOfType
2827
import org.jetbrains.kotlin.resolve.DescriptorToSourceUtils
2928

30-
class ChangeVariableMutabilityFix(element: KtNamedDeclaration, private val makeVar: Boolean) : KotlinQuickFixAction<KtNamedDeclaration>(element) {
29+
class ChangeVariableMutabilityFix(element: KtValVarKeywordOwner, private val makeVar: Boolean) : KotlinQuickFixAction<KtValVarKeywordOwner>(element) {
3130

3231
override fun getText() = if (makeVar) "Make variable mutable" else "Make variable immutable"
3332

3433
override fun getFamilyName(): String = text
3534

3635
override fun isAvailable(project: Project, editor: Editor?, file: PsiFile): Boolean {
37-
return when (element) {
38-
is KtProperty -> element.isVar != makeVar
39-
is KtParameter ->
40-
element.getStrictParentOfType<KtPrimaryConstructor>()?.valueParameterList == element.parent
41-
&& (element.valOrVarKeyword == KtTokens.VAR_KEYWORD) != makeVar
42-
else -> false
43-
}
36+
val valOrVar = element.valOrVarKeyword?.node?.elementType ?: return false
37+
return (valOrVar == KtTokens.VAR_KEYWORD) != makeVar
4438
}
4539

4640
override fun invoke(project: Project, editor: Editor?, file: KtFile) {
4741
val factory = KtPsiFactory(project)
4842
val newKeyword = if (makeVar) factory.createVarKeyword() else factory.createValKeyword()
49-
when (element) {
50-
is KtProperty -> element.valOrVarKeyword.replace(newKeyword)
51-
is KtParameter -> element.valOrVarKeyword!!.replace(newKeyword)
52-
}
43+
element.valOrVarKeyword!!.replace(newKeyword)
5344
}
5445

5546
companion object {
@@ -63,7 +54,7 @@ class ChangeVariableMutabilityFix(element: KtNamedDeclaration, private val makeV
6354
val VAL_REASSIGNMENT_FACTORY: KotlinSingleIntentionActionFactory = object: KotlinSingleIntentionActionFactory() {
6455
override fun createAction(diagnostic: Diagnostic): IntentionAction? {
6556
val propertyDescriptor = Errors.VAL_REASSIGNMENT.cast(diagnostic).a
66-
val declaration = DescriptorToSourceUtils.descriptorToDeclaration(propertyDescriptor) as? KtNamedDeclaration ?: return null
57+
val declaration = DescriptorToSourceUtils.descriptorToDeclaration(propertyDescriptor) as? KtValVarKeywordOwner ?: return null
6758
return ChangeVariableMutabilityFix(declaration, true)
6859
}
6960
}
@@ -72,7 +63,7 @@ class ChangeVariableMutabilityFix(element: KtNamedDeclaration, private val makeV
7263
override fun createAction(diagnostic: Diagnostic): IntentionAction? {
7364
val element = diagnostic.psiElement
7465
return when (element) {
75-
is KtProperty, is KtParameter -> ChangeVariableMutabilityFix(element as KtNamedDeclaration, true)
66+
is KtProperty, is KtParameter -> ChangeVariableMutabilityFix(element as KtValVarKeywordOwner, true)
7667
else -> null
7768
}
7869
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
fun foo(p: Int) {
2+
var v1: Int
3+
var v2: Int = 0
4+
if (p > 0) v1 = 1 else v1 = 2
5+
v2 = 1
6+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
fun foo(p: Int) {
2+
var v: Int
3+
for (i in 1..10) {
4+
v = i
5+
}
6+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
fun foo(p: Int) {
2+
var v: Int
3+
v = 0
4+
if (p > 0) v = 1
5+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
fun foo(p: Int) {
2+
var (v1, v2) = getPair()
3+
print(v1)
4+
}
5+
6+
fun getPair(): Pair<Int, String> = 1 to ""
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
fun foo(p: Int) {
2+
var (v1, v2) = getPair()
3+
print(v1)
4+
v2 = ""
5+
}
6+
7+
fun getPair(): Pair<Int, String> = 1 to ""
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
fun foo(p: Int) {
2+
var (v1, v2) = getPair()
3+
}
4+
5+
fun getPair(): Pair<Int, String> = 1 to ""
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
<problems>
2+
<problem>
3+
<file>withInitializer.kt</file>
4+
<line>4</line>
5+
<module>light_idea_test_case</module>
6+
<entry_point TYPE="file" FQNAME="withInitializer.kt" />
7+
<problem_class severity="WARNING" attribute_key="WARNING_ATTRIBUTES">Local 'var' can be declared as 'val'</problem_class>
8+
<description>Can be declared as 'val'</description>
9+
</problem>
10+
11+
<problem>
12+
<file>alwaysAssigned.kt</file>
13+
<line>2</line>
14+
<module>light_idea_test_case</module>
15+
<entry_point TYPE="file" FQNAME="alwaysAssigned.kt" />
16+
<problem_class severity="WARNING" attribute_key="WARNING_ATTRIBUTES">Local 'var' can be declared as 'val'</problem_class>
17+
<description>Can be declared as 'val'</description>
18+
</problem>
19+
20+
<problem>
21+
<file>notAssignedWhenNotUsed.kt</file>
22+
<line>2</line>
23+
<module>light_idea_test_case</module>
24+
<entry_point TYPE="file" FQNAME="notAssignedWhenNotUsed.kt" />
25+
<problem_class severity="WARNING" attribute_key="WARNING_ATTRIBUTES">Local 'var' can be declared as 'val'</problem_class>
26+
<description>Can be declared as 'val'</description>
27+
</problem>
28+
29+
<problem>
30+
<file>twoVariables.kt</file>
31+
<line>2</line>
32+
<module>light_idea_test_case</module>
33+
<entry_point TYPE="file" FQNAME="twoVariables.kt" />
34+
<problem_class severity="WARNING" attribute_key="WARNING_ATTRIBUTES">Local 'var' can be declared as 'val'</problem_class>
35+
<description>Can be declared as 'val'</description>
36+
</problem>
37+
38+
<problem>
39+
<file>twoVariables.kt</file>
40+
<line>3</line>
41+
<module>light_idea_test_case</module>
42+
<entry_point TYPE="file" FQNAME="twoVariables.kt" />
43+
<problem_class severity="WARNING" attribute_key="WARNING_ATTRIBUTES">Local 'var' can be declared as 'val'</problem_class>
44+
<description>Can be declared as 'val'</description>
45+
</problem>
46+
47+
<problem>
48+
<file>desctructuringDeclaration1.kt</file>
49+
<line>2</line>
50+
<module>light_idea_test_case</module>
51+
<entry_point TYPE="file" FQNAME="desctructuringDeclaration1.kt" />
52+
<problem_class severity="WARNING" attribute_key="WARNING_ATTRIBUTES">Local 'var' can be declared as 'val'</problem_class>
53+
<description>Can be declared as 'val'</description>
54+
</problem>
55+
</problems>
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
// INSPECTION_CLASS: org.jetbrains.kotlin.idea.inspections.CanBeValInspection
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
fun foo(p: Int) {
2+
var v: Int
3+
if (p > 0) {
4+
v = 1
5+
print(v)
6+
}
7+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
var global = 1
2+
3+
class C {
4+
var field = 2
5+
6+
fun foo() {
7+
print(field)
8+
print(global)
9+
}
10+
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
fun foo() {
2+
val v = 1
3+
print(v)
4+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
fun foo(p: Int) {
2+
var v1: Int
3+
var v2: Int
4+
if (p > 0) v1 = 1 else v1 = 2
5+
v2 = 1
6+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
fun foo() {
2+
var v: Int
3+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
fun foo() {
2+
var v = 1
3+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
fun foo() {
2+
var v1 = 1
3+
var v2 = 2
4+
var v3 = 3
5+
v1 = 1
6+
v2++
7+
print(v3)
8+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
org.jetbrains.kotlin.idea.inspections.CanBeValInspection
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
// "Make variable immutable" "true"
2+
fun foo(p: Int) {
3+
<caret>var (v1, v2) = getPair()!!
4+
v1
5+
}
6+
7+
fun getPair(): Pair<Int, String>? = null
8+
9+
data class Pair<T1, T2>(val a: T1, val b: T2)

0 commit comments

Comments
 (0)