[kotlin] k2: port unnecessary variable inspection

name conflicts with implicits are not supported yet

^KTIJ-28465 fixed

GitOrigin-RevId: 17dce08a47a957cc5f2edf209d280e06340f65c5
This commit is contained in:
Anna Kozlova
2025-04-12 14:41:31 +00:00
committed by intellij-monorepo-bot
parent 493bd9ac5f
commit 3c0c804b94
6 changed files with 359 additions and 1 deletions
@@ -754,6 +754,14 @@
key="inspection.variable.initializer.is.redundant.display.name" bundle="messages.KotlinBundle">
</localInspection>
<localInspection implementationClass="org.jetbrains.kotlin.idea.k2.codeinsight.inspections.UnnecessaryVariableInspection"
groupPath="Kotlin"
groupBundle="messages.KotlinBundle" groupKey="group.names.redundant.constructs"
enabledByDefault="true"
level="WARNING"
language="kotlin"
key="inspection.unnecessary.variable.display.name" bundle="messages.KotlinBundle"/>
<localInspection implementationClass="org.jetbrains.kotlin.idea.k2.codeinsight.inspections.diagnosticBased.RemoveRedundantCallsOfConversionMethodsInspection"
groupPath="Kotlin"
groupBundle="messages.KotlinBundle" groupKey="group.names.redundant.constructs"
@@ -0,0 +1,160 @@
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package org.jetbrains.kotlin.idea.k2.codeinsight.inspections
import com.intellij.codeInspection.LocalQuickFix
import com.intellij.codeInspection.ProblemDescriptor
import com.intellij.codeInspection.ProblemHighlightType
import com.intellij.codeInspection.ProblemsHolder
import com.intellij.codeInspection.options.OptPane
import com.intellij.codeInspection.options.OptPane.checkbox
import com.intellij.codeInspection.options.OptPane.pane
import com.intellij.openapi.project.Project
import com.intellij.openapi.util.TextRange
import com.intellij.psi.PsiComment
import com.intellij.psi.PsiElement
import com.intellij.psi.PsiWhiteSpace
import com.intellij.psi.impl.source.tree.LeafPsiElement
import com.intellij.psi.search.LocalSearchScope
import com.intellij.psi.search.searches.ReferencesSearch
import org.jetbrains.kotlin.analysis.api.KaSession
import org.jetbrains.kotlin.analysis.api.analyze
import org.jetbrains.kotlin.analysis.api.symbols.KaFunctionSymbol
import org.jetbrains.kotlin.analysis.api.symbols.KaLocalVariableSymbol
import org.jetbrains.kotlin.analysis.api.symbols.KaParameterSymbol
import org.jetbrains.kotlin.analysis.api.symbols.markers.KaNamedSymbol
import org.jetbrains.kotlin.idea.base.codeInsight.KotlinDeclarationNameValidator
import org.jetbrains.kotlin.idea.base.codeInsight.KotlinNameSuggestionProvider
import org.jetbrains.kotlin.idea.base.psi.getLineNumber
import org.jetbrains.kotlin.idea.base.psi.isMultiLine
import org.jetbrains.kotlin.idea.base.resources.KotlinBundle
import org.jetbrains.kotlin.idea.codeinsight.api.classic.inspections.AbstractKotlinInspection
import org.jetbrains.kotlin.idea.k2.refactoring.inline.KotlinInlinePropertyHandler
import org.jetbrains.kotlin.idea.references.mainReference
import org.jetbrains.kotlin.lexer.KtTokens
import org.jetbrains.kotlin.psi.*
import org.jetbrains.kotlin.psi.psiUtil.*
import org.jetbrains.kotlin.utils.addIfNotNull
import org.jetbrains.kotlin.utils.addToStdlib.firstIsInstanceOrNull
import org.jetbrains.kotlin.utils.addToStdlib.safeAs
internal class UnnecessaryVariableInspection : AbstractKotlinInspection() {
private enum class Status {
RETURN_ONLY,
EXACT_COPY
}
@JvmField
var reportImmediatelyReturnedVariables = false
override fun getOptionsPane(): OptPane = pane(
checkbox(
"reportImmediatelyReturnedVariables",
KotlinBundle.message("inspection.unnecessary.variable.option.report.immediately.returned.variables")
)
)
override fun buildVisitor(holder: ProblemsHolder, isOnTheFly: Boolean) = propertyVisitor { property ->
if (!property.isLocal || property.annotationEntries.isNotEmpty() || property.hasComment() || property.hasDelegate()) {
return@propertyVisitor
}
val status = statusFor(property)
if (status != null) {
val description = when (status) {
Status.RETURN_ONLY -> KotlinBundle.message("variable.used.only.in.following.return.and.should.be.inlined")
Status.EXACT_COPY -> KotlinBundle.message(
"variable.is.same.as.0.and.should.be.inlined",
(property.initializer as? KtNameReferenceExpression)?.getReferencedName().toString()
)
}
val hasMultiLineBlock = property.initializer?.hasMultiLineBlock() == true
val highlightType = if (hasMultiLineBlock) ProblemHighlightType.INFORMATION else ProblemHighlightType.GENERIC_ERROR_OR_WARNING
val nameIdentifier = property.nameIdentifier
val range = if (nameIdentifier != null) TextRange(0, nameIdentifier.textLength) else null
holder.registerProblemWithoutOfflineInformation(
property.nameIdentifier ?: property,
description,
isOnTheFly,
highlightType,
range,
InlineVariableFix()
)
}
}
private fun statusFor(property: KtProperty): Status? {
val enclosingElement = KtPsiUtil.getEnclosingElementForLocalDeclaration(property) ?: return null
val initializer = property.initializer ?: return null
fun KaSession.isExactCopy(): Boolean {
if (property.isVar || initializer !is KtNameReferenceExpression || property.typeReference != null) return false
val symbol = initializer.mainReference.resolveToSymbol()
val initializerSymbol = symbol as? KaLocalVariableSymbol ?: symbol as? KaParameterSymbol ?: return false
val isVal = initializerSymbol.isVal
val isContainingSymbolFunction = initializerSymbol.containingSymbol is KaFunctionSymbol
val hasDelegate = (initializerSymbol.psi as? KtProperty)?.hasDelegate() == true
if (!isVal || !isContainingSymbolFunction || hasDelegate) return false
val copyName = initializerSymbol.name.asString()
if (ReferencesSearch.search(property, LocalSearchScope(enclosingElement)).findFirst() == null) return false
val excludedDeclaration = initializerSymbol.psi as? KtDeclaration
val nameValidator = KotlinDeclarationNameValidator(
visibleDeclarationsContext = enclosingElement,
checkVisibleDeclarationsContext = true,
target = KotlinNameSuggestionProvider.ValidatorTarget.VARIABLE,
excludedDeclarations = listOfNotNull(excludedDeclaration)
)
return nameValidator.validate(copyName)
}
fun KaSession.isReturnOnly(): Boolean {
val nextStatement = property.getNextSiblingIgnoringWhitespaceAndComments() as? KtReturnExpression ?: return false
val returned = nextStatement.returnedExpression as? KtNameReferenceExpression ?: return false
val returnedSymbol = returned.mainReference.resolveToSymbol() as? KaNamedSymbol ?: return false
val elementSymbol = property.symbol as? KaNamedSymbol ?: return false
return returnedSymbol == elementSymbol
}
return when {
analyze(property) { isExactCopy() } -> Status.EXACT_COPY
reportImmediatelyReturnedVariables && analyze(property) { isReturnOnly() } -> Status.RETURN_ONLY
else -> null
}
}
private class InlineVariableFix : LocalQuickFix {
override fun getFamilyName(): String = KotlinBundle.message("inline.variable")
override fun startInWriteAction(): Boolean = false
override fun applyFix(project: Project, descriptor: ProblemDescriptor) {
val element = descriptor.psiElement.parent as? KtProperty ?: return
KotlinInlinePropertyHandler().inlineElement(project, null, element)
}
}
private fun LeafPsiElement.startsMultilineBlock(): Boolean =
node.elementType == KtTokens.LBRACE && parent.safeAs<KtExpression>()?.isMultiLine() == true
private fun KtExpression.hasMultiLineBlock(): Boolean =
anyDescendantOfType<LeafPsiElement> { it.startsMultilineBlock() }
private fun KtProperty.hasComment(): Boolean {
fun Sequence<PsiElement>.firstComment() =
takeWhile { it is PsiWhiteSpace || it is PsiComment }.firstIsInstanceOrNull<PsiComment>()
return prevLeafs.firstComment() != null ||
initializer?.nextLeafs?.firstComment()?.takeIf { it.getLineNumber() == this.getLineNumber() } != null
}
}
@@ -8306,6 +8306,193 @@ public abstract class K2LocalInspectionTestGenerated extends AbstractK2LocalInsp
}
}
@RunWith(JUnit3RunnerWithInners.class)
@TestMetadata("../../../idea/tests/testData/inspectionsLocal/unnecessaryVariable")
public abstract static class UnnecessaryVariable extends AbstractK2LocalInspectionTest {
@RunWith(JUnit3RunnerWithInners.class)
@TestMetadata("../../../idea/tests/testData/inspectionsLocal/unnecessaryVariable/reportImmediatelyReturnedVariables")
public static class ReportImmediatelyReturnedVariables extends AbstractK2LocalInspectionTest {
@java.lang.Override
@org.jetbrains.annotations.NotNull
public final KotlinPluginMode getPluginMode() {
return KotlinPluginMode.K2;
}
private void runTest(String testDataFilePath) throws Exception {
KotlinTestUtils.runTest(this::doTest, this, testDataFilePath);
}
@TestMetadata("ifElse.kt")
public void testIfElse() throws Exception {
runTest("../../../idea/tests/testData/inspectionsLocal/unnecessaryVariable/reportImmediatelyReturnedVariables/ifElse.kt");
}
@TestMetadata("ifElse2.kt")
public void testIfElse2() throws Exception {
runTest("../../../idea/tests/testData/inspectionsLocal/unnecessaryVariable/reportImmediatelyReturnedVariables/ifElse2.kt");
}
@TestMetadata("ifElse3.kt")
public void testIfElse3() throws Exception {
runTest("../../../idea/tests/testData/inspectionsLocal/unnecessaryVariable/reportImmediatelyReturnedVariables/ifElse3.kt");
}
@TestMetadata("ifElse4.kt")
public void testIfElse4() throws Exception {
runTest("../../../idea/tests/testData/inspectionsLocal/unnecessaryVariable/reportImmediatelyReturnedVariables/ifElse4.kt");
}
@TestMetadata("ifElse5.kt")
public void testIfElse5() throws Exception {
runTest("../../../idea/tests/testData/inspectionsLocal/unnecessaryVariable/reportImmediatelyReturnedVariables/ifElse5.kt");
}
@TestMetadata("lambda.kt")
public void testLambda() throws Exception {
runTest("../../../idea/tests/testData/inspectionsLocal/unnecessaryVariable/reportImmediatelyReturnedVariables/lambda.kt");
}
@TestMetadata("lambda2.kt")
public void testLambda2() throws Exception {
runTest("../../../idea/tests/testData/inspectionsLocal/unnecessaryVariable/reportImmediatelyReturnedVariables/lambda2.kt");
}
@TestMetadata("simpleReturn.kt")
public void testSimpleReturn() throws Exception {
runTest("../../../idea/tests/testData/inspectionsLocal/unnecessaryVariable/reportImmediatelyReturnedVariables/simpleReturn.kt");
}
@TestMetadata("try.kt")
public void testTry() throws Exception {
runTest("../../../idea/tests/testData/inspectionsLocal/unnecessaryVariable/reportImmediatelyReturnedVariables/try.kt");
}
@TestMetadata("when.kt")
public void testWhen() throws Exception {
runTest("../../../idea/tests/testData/inspectionsLocal/unnecessaryVariable/reportImmediatelyReturnedVariables/when.kt");
}
}
@RunWith(JUnit3RunnerWithInners.class)
@TestMetadata("../../../idea/tests/testData/inspectionsLocal/unnecessaryVariable")
public static class Uncategorized extends AbstractK2LocalInspectionTest {
@java.lang.Override
@org.jetbrains.annotations.NotNull
public final KotlinPluginMode getPluginMode() {
return KotlinPluginMode.K2;
}
private void runTest(String testDataFilePath) throws Exception {
KotlinTestUtils.runTest(this::doTest, this, testDataFilePath);
}
@TestMetadata("copyOfVal.kt")
public void testCopyOfVal() throws Exception {
runTest("../../../idea/tests/testData/inspectionsLocal/unnecessaryVariable/copyOfVal.kt");
}
@TestMetadata("copyOfValInLambda.kt")
public void testCopyOfValInLambda() throws Exception {
runTest("../../../idea/tests/testData/inspectionsLocal/unnecessaryVariable/copyOfValInLambda.kt");
}
@TestMetadata("copyOfValInLambda2.kt")
public void testCopyOfValInLambda2() throws Exception {
runTest("../../../idea/tests/testData/inspectionsLocal/unnecessaryVariable/copyOfValInLambda2.kt");
}
@TestMetadata("copyOfValInLambda3.kt")
public void testCopyOfValInLambda3() throws Exception {
runTest("../../../idea/tests/testData/inspectionsLocal/unnecessaryVariable/copyOfValInLambda3.kt");
}
@TestMetadata("copyOfValUnused.kt")
public void testCopyOfValUnused() throws Exception {
runTest("../../../idea/tests/testData/inspectionsLocal/unnecessaryVariable/copyOfValUnused.kt");
}
@TestMetadata("copyOfValWithExplicitType.kt")
public void testCopyOfValWithExplicitType() throws Exception {
runTest("../../../idea/tests/testData/inspectionsLocal/unnecessaryVariable/copyOfValWithExplicitType.kt");
}
@TestMetadata("copyOfVar.kt")
public void testCopyOfVar() throws Exception {
runTest("../../../idea/tests/testData/inspectionsLocal/unnecessaryVariable/copyOfVar.kt");
}
@TestMetadata("delegated.kt")
public void testDelegated() throws Exception {
runTest("../../../idea/tests/testData/inspectionsLocal/unnecessaryVariable/delegated.kt");
}
@TestMetadata("hasAnnotation.kt")
public void testHasAnnotation() throws Exception {
runTest("../../../idea/tests/testData/inspectionsLocal/unnecessaryVariable/hasAnnotation.kt");
}
@TestMetadata("hasComment.kt")
public void testHasComment() throws Exception {
runTest("../../../idea/tests/testData/inspectionsLocal/unnecessaryVariable/hasComment.kt");
}
@TestMetadata("hasComment2.kt")
public void testHasComment2() throws Exception {
runTest("../../../idea/tests/testData/inspectionsLocal/unnecessaryVariable/hasComment2.kt");
}
@TestMetadata("hasComment3.kt")
public void testHasComment3() throws Exception {
runTest("../../../idea/tests/testData/inspectionsLocal/unnecessaryVariable/hasComment3.kt");
}
@TestMetadata("it.kt")
public void testIt() throws Exception {
runTest("../../../idea/tests/testData/inspectionsLocal/unnecessaryVariable/it.kt");
}
@TestMetadata("override.kt")
public void testOverride() throws Exception {
runTest("../../../idea/tests/testData/inspectionsLocal/unnecessaryVariable/override.kt");
}
@TestMetadata("paramCopy.kt")
public void testParamCopy() throws Exception {
runTest("../../../idea/tests/testData/inspectionsLocal/unnecessaryVariable/paramCopy.kt");
}
@TestMetadata("parameterWithSameNameBelow.kt")
public void testParameterWithSameNameBelow() throws Exception {
runTest("../../../idea/tests/testData/inspectionsLocal/unnecessaryVariable/parameterWithSameNameBelow.kt");
}
@TestMetadata("propertyCopy.kt")
public void testPropertyCopy() throws Exception {
runTest("../../../idea/tests/testData/inspectionsLocal/unnecessaryVariable/propertyCopy.kt");
}
@TestMetadata("simpleReturn.kt")
public void testSimpleReturn() throws Exception {
runTest("../../../idea/tests/testData/inspectionsLocal/unnecessaryVariable/simpleReturn.kt");
}
@TestMetadata("topLevelCopy.kt")
public void testTopLevelCopy() throws Exception {
runTest("../../../idea/tests/testData/inspectionsLocal/unnecessaryVariable/topLevelCopy.kt");
}
@TestMetadata("varCopy.kt")
public void testVarCopy() throws Exception {
runTest("../../../idea/tests/testData/inspectionsLocal/unnecessaryVariable/varCopy.kt");
}
@TestMetadata("whenSubject.kt")
public void testWhenSubject() throws Exception {
runTest("../../../idea/tests/testData/inspectionsLocal/unnecessaryVariable/whenSubject.kt");
}
}
}
@RunWith(JUnit3RunnerWithInners.class)
@TestMetadata("../../../idea/tests/testData/inspectionsLocal/canBeParameter")
public static class CanBeParameter extends AbstractK2LocalInspectionTest {
@@ -0,0 +1 @@
org.jetbrains.kotlin.idea.k2.codeinsight.inspections.UnnecessaryVariableInspection
@@ -8,4 +8,5 @@ fun foo(a: List<String>, b: List<Int>) {
println(a2.length)
}
}
}
}
// IGNORE_K2
@@ -70,6 +70,7 @@ internal fun MutableTWorkspace.generateK2InspectionTests() {
model("${idea}/inspectionsLocal/redundantUnitReturnType")
model("${idea}/inspectionsLocal/suspiciousCollectionReassignment")
model("${idea}/inspectionsLocal/suspiciousVarProperty")
model("${idea}/inspectionsLocal/unnecessaryVariable")
model("${idea}/inspectionsLocal/canBeParameter")
model("${idea}/inspectionsLocal/arrayInDataClass")
model("${idea}/inspectionsLocal/collections/simplifiableCallChain")