[java-intentions] IDEA-379168 Suggest renaming underscore variable when it's used.

GitOrigin-RevId: eb0cfd7ddf8d5d730198ea5cd064cca79724348e
This commit is contained in:
Tagir Valeev
2025-09-11 18:13:46 +00:00
committed by intellij-monorepo-bot
parent 719fe48b48
commit 2bd42bb487
20 changed files with 260 additions and 9 deletions
@@ -21,6 +21,7 @@ default.language.level.description=SDK default
element.abstract_method=abstract method
element.annotation=annotation
element.anonymous_class=anonymous class
element.catch_parameter=catch parameter
element.class=class
element.type.parameter=type parameter
element.type.parameters=type parameters
@@ -30,9 +31,11 @@ element.enum=enum
element.enum_constant=enum constant
element.expression=expression
element.field=field
element.for_parameter='for' loop parameter
element.initializer=initializer
element.interface=interface
element.label=label
element.lambda_parameter=lambda parameter
element.local_variable=local variable
element.method=method
element.module=module
@@ -3,10 +3,7 @@ package com.intellij.codeInspection;
import com.intellij.codeInsight.daemon.impl.analysis.AbstractJavaErrorFixProvider;
import com.intellij.codeInsight.daemon.impl.analysis.HighlightFixUtil;
import com.intellij.codeInsight.daemon.impl.quickfix.AddExceptionToCatchFix;
import com.intellij.codeInsight.daemon.impl.quickfix.AddFinallyFix;
import com.intellij.codeInsight.daemon.impl.quickfix.InsertMissingTokenFix;
import com.intellij.codeInsight.daemon.impl.quickfix.VariableAccessFromInnerClassJava10Fix;
import com.intellij.codeInsight.daemon.impl.quickfix.*;
import com.intellij.codeInsight.intention.CommonIntentionAction;
import com.intellij.codeInspection.streamMigration.SimplifyForEachInspection;
import com.intellij.core.JavaPsiBundle;
@@ -17,6 +14,8 @@ import org.jetbrains.annotations.NotNull;
import java.util.function.Consumer;
import static com.intellij.java.codeserver.highlighting.errors.JavaErrorKinds.UNDERSCORE_IDENTIFIER_UNNAMED;
/**
* Some quick-fixes not accessible from the java.analysis module are registered here.
*/
@@ -26,6 +25,9 @@ public final class AdditionalJavaErrorFixProvider extends AbstractJavaErrorFixPr
fix(JavaErrorKinds.VARIABLE_MUST_BE_EFFECTIVELY_FINAL_LAMBDA, error -> new VariableAccessFromInnerClassJava10Fix(error.psi()));
fix(JavaErrorKinds.VARIABLE_MUST_BE_EFFECTIVELY_FINAL_GUARD, error -> new VariableAccessFromInnerClassJava10Fix(error.psi()));
fixes(JavaErrorKinds.SYNTAX_ERROR, (error, info) -> registerErrorElementFixes(info, error.psi()));
fix(UNDERSCORE_IDENTIFIER_UNNAMED, error -> error.psi().getParent() instanceof PsiReferenceExpression ref &&
"_".equals(ref.getReferenceName()) ?
new RenameUnderscoreFix(ref) : null);
}
private static void registerErrorElementFixes(@NotNull Consumer<? super CommonIntentionAction> info,
@@ -0,0 +1,81 @@
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.codeInsight.daemon.impl.quickfix;
import com.intellij.java.JavaBundle;
import com.intellij.modcommand.*;
import com.intellij.openapi.util.Key;
import com.intellij.psi.*;
import com.intellij.psi.codeStyle.VariableKind;
import com.intellij.psi.scope.ElementClassHint;
import com.intellij.psi.scope.PsiScopeProcessor;
import com.intellij.psi.scope.util.PsiScopesUtil;
import com.intellij.psi.util.JavaElementKind;
import com.intellij.psi.util.PsiUtil;
import com.intellij.util.containers.ContainerUtil;
import com.siyeh.ig.psiutils.VariableNameGenerator;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.ArrayList;
import java.util.List;
public final class RenameUnderscoreFix extends PsiBasedModCommandAction<PsiReferenceExpression> {
public RenameUnderscoreFix(@NotNull PsiReferenceExpression element) {
super(element);
}
@Override
protected @Nullable Presentation getPresentation(@NotNull ActionContext context, @NotNull PsiReferenceExpression element) {
if (findDeclarations(element).isEmpty()) return null;
return super.getPresentation(context, element);
}
@Override
protected @NotNull ModCommand perform(@NotNull ActionContext context, @NotNull PsiReferenceExpression element) {
List<PsiVariable> declarations = findDeclarations(element);
List<@NotNull ModCommandAction> actions = ContainerUtil.map(declarations, var -> ModCommand.psiUpdateStep(
var, JavaBundle.message("intention.rename.underscore.name", JavaElementKind.fromElement(var).subject(), var.getType().getPresentableText()), (v, updater) -> {
if (!element.isValid()) return;
PsiReferenceExpression writableRef = updater.getWritable(element);
List<String> names = new VariableNameGenerator(v, VariableKind.LOCAL_VARIABLE)
.byType(v.getType())
.generateAll(true);
String defaultName = names.getFirst();
v.setName(defaultName);
writableRef.replace(JavaPsiFacade.getElementFactory(context.project()).createExpressionFromText(defaultName, writableRef));
updater.rename(v, names);
}, v -> v.getTextRange()));
return ModCommand.chooseAction(JavaBundle.message("intention.rename.underscore.popup.title"), actions);
}
@Override
public @NotNull String getFamilyName() {
return JavaBundle.message("intention.rename.underscore.family.name");
}
public static List<PsiVariable> findDeclarations(@NotNull PsiReferenceExpression expression) {
List<PsiVariable> result = new ArrayList<>();
PsiScopesUtil.treeWalkUp(new PsiScopeProcessor() {
@SuppressWarnings("unchecked")
@Override
public <T> @Nullable T getHint(@NotNull Key<T> hintKey) {
if (hintKey == ElementClassHint.KEY) {
return (T)(ElementClassHint)(ElementClassHint.DeclarationKind.VARIABLE::equals);
}
if (hintKey == ElementClassHint.PROCESS_UNNAMED_VARIABLES) {
return (T)Boolean.TRUE;
}
return null;
}
@Override
public boolean execute(@NotNull PsiElement element, @NotNull ResolveState state) {
if (element instanceof PsiVariable variable && PsiUtil.isJvmLocalVariable(variable) && variable.isUnnamed()) {
result.add(variable);
}
return true;
}
}, expression, expression.getContainingFile());
return result;
}
}
@@ -16,6 +16,7 @@ public enum JavaElementKind {
ABSTRACT_METHOD("element.abstract_method"),
ANNOTATION("element.annotation"),
ANONYMOUS_CLASS("element.anonymous_class"),
CATCH_PARAMETER("element.catch_parameter"),
CLASS("element.class"),
CONSTANT("element.constant"),
CONSTRUCTOR("element.constructor"),
@@ -24,9 +25,11 @@ public enum JavaElementKind {
EXPRESSION("element.expression"),
EXTENDS_LIST("element.extends.list"),
FIELD("element.field"),
FOR_PARAMETER("element.for_parameter"),
INITIALIZER("element.initializer"),
INTERFACE("element.interface"),
LABEL("element.label"),
LAMBDA_PARAMETER("element.lambda_parameter"),
LOCAL_VARIABLE("element.local_variable"),
METHOD("element.method"),
METHOD_CALL("element.method.call"),
@@ -86,6 +89,10 @@ public enum JavaElementKind {
case TYPE_PARAMETER:
case ANONYMOUS_CLASS:
return CLASS;
case LAMBDA_PARAMETER:
case CATCH_PARAMETER:
case FOR_PARAMETER:
return PARAMETER;
default:
return this;
}
@@ -171,6 +178,16 @@ public enum JavaElementKind {
return PATTERN_VARIABLE;
}
if (element instanceof PsiParameter) {
PsiElement scope = ((PsiParameter)element).getDeclarationScope();
if (scope instanceof PsiForeachStatement) {
return FOR_PARAMETER;
}
if (scope instanceof PsiLambdaExpression) {
return LAMBDA_PARAMETER;
}
if (scope instanceof PsiCatchSection) {
return CATCH_PARAMETER;
}
return PARAMETER;
}
if (element instanceof PsiReceiverParameter) {
@@ -204,7 +204,10 @@ public final class PsiImplUtil {
if (fromBody) {
final PsiParameter[] parameters = element.getParameterList().getParameters();
for (PsiParameter parameter : parameters) {
if (parameter.isUnnamed()) continue;
if (parameter.isUnnamed() &&
!Boolean.TRUE.equals(processor.getHint(ElementClassHint.PROCESS_UNNAMED_VARIABLES))) {
continue;
}
if (!processor.execute(parameter, state)) return false;
}
}
@@ -222,7 +225,8 @@ public final class PsiImplUtil {
for (PsiResourceListElement resource : resourceList) {
if (resource == lastParent) break;
if (resource instanceof PsiResourceVariable &&
!((PsiResourceVariable)resource).isUnnamed() &&
!(((PsiResourceVariable)resource).isUnnamed() &&
!Boolean.TRUE.equals(processor.getHint(ElementClassHint.PROCESS_UNNAMED_VARIABLES))) &&
!processor.execute(resource, state)) return false;
}
@@ -119,7 +119,8 @@ public class PsiDeclarationStatementImpl extends CompositePsiElement implements
for (PsiElement element : getDeclaredElements()) {
if (element != lastParent) {
if (element instanceof PsiVariable && ((PsiVariable)element).isUnnamed()) continue;
if (element instanceof PsiVariable && ((PsiVariable)element).isUnnamed() &&
!Boolean.TRUE.equals(processor.getHint(ElementClassHint.PROCESS_UNNAMED_VARIABLES))) continue;
if (!processor.execute(element, state)) return false;
}
else {
@@ -4,6 +4,7 @@ package com.intellij.psi.impl.source.tree.java;
import com.intellij.psi.*;
import com.intellij.psi.impl.source.Constants;
import com.intellij.psi.impl.source.tree.ChildRole;
import com.intellij.psi.scope.ElementClassHint;
import com.intellij.psi.scope.PsiScopeProcessor;
import org.jetbrains.annotations.NotNull;
@@ -32,7 +33,8 @@ public class PsiForeachStatementImpl extends PsiForeachStatementBaseImpl impleme
return true;
PsiParameter parameter = getIterationParameter();
if (parameter.isUnnamed()) return true;
if (parameter.isUnnamed() &&
!Boolean.TRUE.equals(processor.getHint(ElementClassHint.PROCESS_UNNAMED_VARIABLES))) return true;
return processor.execute(parameter, state);
}
@@ -4,6 +4,7 @@ package com.intellij.psi.impl.source.tree.java;
import com.intellij.psi.*;
import com.intellij.psi.impl.source.Constants;
import com.intellij.psi.impl.source.tree.CompositePsiElement;
import com.intellij.psi.scope.ElementClassHint;
import com.intellij.psi.scope.PsiScopeProcessor;
import com.intellij.psi.util.PsiTreeUtil;
import org.jetbrains.annotations.NotNull;
@@ -46,7 +47,8 @@ public class PsiTypeTestPatternImpl extends CompositePsiElement implements PsiTy
processor.handleEvent(PsiScopeProcessor.Event.SET_DECLARATION_HOLDER, this);
PsiPatternVariable variable = getPatternVariable();
if (variable != null && variable != lastParent && !variable.isUnnamed()) {
if (variable != null && variable != lastParent &&
!(variable.isUnnamed() && !Boolean.TRUE.equals(processor.getHint(ElementClassHint.PROCESS_UNNAMED_VARIABLES)))) {
return processor.execute(variable, state);
}
return true;
@@ -7,6 +7,11 @@ import org.jetbrains.annotations.NotNull;
public interface ElementClassHint {
Key<ElementClassHint> KEY = Key.create("ElementClassHint");
/**
* If this hint is set to true, then the unnamed variables will be processed. By default, they are skipped.
*/
Key<Boolean> PROCESS_UNNAMED_VARIABLES = Key.create("ElementClassHint.PROCESS_UNNAMED_VARIABLES");
enum DeclarationKind {
CLASS,
PACKAGE,
@@ -0,0 +1,16 @@
// "Rename unnamed variable|->Rename catch parameter of type NumberFormatException" "true-preview"
import java.util.function.BiConsumer;
public class JavaTest {
public static void main(String[] args) {
BiConsumer<String, Integer> cons = (_, _) -> {
for (int _ : new int[10]) {
try {
Integer.parseInt("123s");
} catch (NumberFormatException numberFormatException) {
System.out.println(numberFormatException);
}
}
};
}
}
@@ -0,0 +1,16 @@
// "Rename unnamed variable|->Rename 'for' loop parameter of type int" "true-preview"
import java.util.function.BiConsumer;
public class JavaTest {
public static void main(String[] args) {
BiConsumer<String, Integer> cons = (_, _) -> {
for (int i : new int[10]) {
try {
Integer.parseInt("123s");
} catch (NumberFormatException _) {
System.out.println(i);
}
}
};
}
}
@@ -0,0 +1,16 @@
// "Rename unnamed variable|->Rename lambda parameter of type String" "true-preview"
import java.util.function.BiConsumer;
public class JavaTest {
public static void main(String[] args) {
BiConsumer<String, Integer> cons = (s, _) -> {
for (int _ : new int[10]) {
try {
Integer.parseInt("123s");
} catch (NumberFormatException _) {
System.out.println(s);
}
}
};
}
}
@@ -0,0 +1,6 @@
// "Rename unnamed variable" "true-preview"
class X {
void test(int i) {
System.out.println(i);
}
}
@@ -0,0 +1,16 @@
// "Rename unnamed variable|->Rename catch parameter of type NumberFormatException" "true-preview"
import java.util.function.BiConsumer;
public class JavaTest {
public static void main(String[] args) {
BiConsumer<String, Integer> cons = (_, _) -> {
for (int _ : new int[10]) {
try {
Integer.parseInt("123s");
} catch (NumberFormatException _) {
System.out.println(<caret>_);
}
}
};
}
}
@@ -0,0 +1,16 @@
// "Rename unnamed variable|->Rename 'for' loop parameter of type int" "true-preview"
import java.util.function.BiConsumer;
public class JavaTest {
public static void main(String[] args) {
BiConsumer<String, Integer> cons = (_, _) -> {
for (int _ : new int[10]) {
try {
Integer.parseInt("123s");
} catch (NumberFormatException _) {
System.out.println(<caret>_);
}
}
};
}
}
@@ -0,0 +1,16 @@
// "Rename unnamed variable|->Rename lambda parameter of type String" "true-preview"
import java.util.function.BiConsumer;
public class JavaTest {
public static void main(String[] args) {
BiConsumer<String, Integer> cons = (_, _) -> {
for (int _ : new int[10]) {
try {
Integer.parseInt("123s");
} catch (NumberFormatException _) {
System.out.println(<caret>_);
}
}
};
}
}
@@ -0,0 +1,6 @@
// "Rename unnamed variable" "true-preview"
class X {
void test(int _) {
System.out.println(<caret>_);
}
}
@@ -0,0 +1,12 @@
// "Rename unnamed variable" "false"
class X {
void test() {
System.out.println(<caret>_);
}
void other() {
for (var _ : new int[10]) {
}
}
}
@@ -0,0 +1,11 @@
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.codeInsight.daemon.impl.quickfix;
import com.intellij.codeInsight.daemon.quickFix.LightQuickFixParameterizedTestCase;
public class RenameUnderscoreTest extends LightQuickFixParameterizedTestCase {
@Override
protected String getBasePath() {
return "/codeInsight/daemonCodeAnalyzer/quickFix/renameUnderscore";
}
}
@@ -1987,3 +1987,6 @@ preview.api.usage={0} is a preview API and may be removed in a future release
preview.api.usage.reflective={0} is a reflective preview API and may be removed in a future release
todo.0=todo "{0}"
todo.item=todo item
intention.rename.underscore.name=Rename {0} of type {1}
intention.rename.underscore.popup.title=Select Declaration
intention.rename.underscore.family.name=Rename unnamed variable