diff --git a/plugins/kotlin/code-insight/inspections-k2/resources/kotlin.code-insight.inspections.k2.xml b/plugins/kotlin/code-insight/inspections-k2/resources/kotlin.code-insight.inspections.k2.xml index d6c5b5d0864e..ada821161844 100644 --- a/plugins/kotlin/code-insight/inspections-k2/resources/kotlin.code-insight.inspections.k2.xml +++ b/plugins/kotlin/code-insight/inspections-k2/resources/kotlin.code-insight.inspections.k2.xml @@ -754,6 +754,14 @@ key="inspection.variable.initializer.is.redundant.display.name" bundle="messages.KotlinBundle"> + + + 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()?.isMultiLine() == true + + private fun KtExpression.hasMultiLineBlock(): Boolean = + anyDescendantOfType { it.startsMultilineBlock() } + + private fun KtProperty.hasComment(): Boolean { + fun Sequence.firstComment() = + takeWhile { it is PsiWhiteSpace || it is PsiComment }.firstIsInstanceOrNull() + return prevLeafs.firstComment() != null || + initializer?.nextLeafs?.firstComment()?.takeIf { it.getLineNumber() == this.getLineNumber() } != null + } +} diff --git a/plugins/kotlin/code-insight/inspections-k2/tests/test/org/jetbrains/kotlin/idea/k2/inspections/tests/K2LocalInspectionTestGenerated.java b/plugins/kotlin/code-insight/inspections-k2/tests/test/org/jetbrains/kotlin/idea/k2/inspections/tests/K2LocalInspectionTestGenerated.java index dc9163c23e7d..350ff3f13c9e 100644 --- a/plugins/kotlin/code-insight/inspections-k2/tests/test/org/jetbrains/kotlin/idea/k2/inspections/tests/K2LocalInspectionTestGenerated.java +++ b/plugins/kotlin/code-insight/inspections-k2/tests/test/org/jetbrains/kotlin/idea/k2/inspections/tests/K2LocalInspectionTestGenerated.java @@ -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 { diff --git a/plugins/kotlin/idea/tests/testData/inspectionsLocal/unnecessaryVariable/.k2Inspection b/plugins/kotlin/idea/tests/testData/inspectionsLocal/unnecessaryVariable/.k2Inspection new file mode 100644 index 000000000000..3a91db00514b --- /dev/null +++ b/plugins/kotlin/idea/tests/testData/inspectionsLocal/unnecessaryVariable/.k2Inspection @@ -0,0 +1 @@ +org.jetbrains.kotlin.idea.k2.codeinsight.inspections.UnnecessaryVariableInspection \ No newline at end of file diff --git a/plugins/kotlin/idea/tests/testData/inspectionsLocal/unnecessaryVariable/it.kt b/plugins/kotlin/idea/tests/testData/inspectionsLocal/unnecessaryVariable/it.kt index ce65bce91fa2..b75e3dde1e63 100644 --- a/plugins/kotlin/idea/tests/testData/inspectionsLocal/unnecessaryVariable/it.kt +++ b/plugins/kotlin/idea/tests/testData/inspectionsLocal/unnecessaryVariable/it.kt @@ -8,4 +8,5 @@ fun foo(a: List, b: List) { println(a2.length) } } -} \ No newline at end of file +} +// IGNORE_K2 \ No newline at end of file diff --git a/plugins/kotlin/util/test-generator-fir/test/org/jetbrains/kotlin/fir/testGenerator/codeinsight/GenerateK2InspectionTests.kt b/plugins/kotlin/util/test-generator-fir/test/org/jetbrains/kotlin/fir/testGenerator/codeinsight/GenerateK2InspectionTests.kt index 9aa30b321c06..067010c179b2 100644 --- a/plugins/kotlin/util/test-generator-fir/test/org/jetbrains/kotlin/fir/testGenerator/codeinsight/GenerateK2InspectionTests.kt +++ b/plugins/kotlin/util/test-generator-fir/test/org/jetbrains/kotlin/fir/testGenerator/codeinsight/GenerateK2InspectionTests.kt @@ -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")