[java-imports] IDEA-13817 Create "favorites" for static import and completion

- part of "favorites" for static import. Inspection and refactoring

GitOrigin-RevId: a764a70000c3f0918e50a814908e5969b93688d7
This commit is contained in:
Mikhail Pyltsin
2025-01-16 14:21:49 +00:00
committed by intellij-monorepo-bot
parent b1796575e6
commit 3cb38fccaa
18 changed files with 421 additions and 30 deletions
@@ -2292,6 +2292,10 @@
<registryKey defaultValue="false"
description="Use shallow unused declaration analysis in batch mode (like in the editor, Java only)"
key="java.unused.declaration.inspection.use.shallow.local.anaysis.as.global"/>
<localInspection groupPath="Java" language="JAVA" key="inspection.static.import.can.be.used.display.name"
groupBundle="messages.InspectionsBundle" groupKey="group.names.imports" enabledByDefault="true"
level="WARNING" cleanupTool="true" editorAttributes="NOT_USED_ELEMENT_ATTRIBUTES"
implementationClass="com.intellij.codeInspection.StaticImportCanBeUsedInspection" bundle="messages.JavaBundle"/>
<intentionAction>
<language>JAVA</language>
@@ -43,8 +43,7 @@ public class JavaProjectCodeInsightSettings implements PersistentStateComponent<
public boolean isStaticAutoImportClass(@Nullable String name) {
if (name == null) return false;
return includedAutoStaticNames.contains(name) ||
JavaIdeCodeInsightSettings.getInstance().includedAutoStaticNames.contains(name);
return getAllIncludedAutoStaticNames().contains(name);
}
public boolean isExcluded(@NotNull String name) {
@@ -16,6 +16,7 @@ import com.intellij.openapi.util.Ref;
import com.intellij.openapi.wm.WindowManager;
import com.intellij.pom.java.JavaFeature;
import com.intellij.psi.*;
import com.intellij.psi.codeStyle.JavaCodeStyleManager;
import com.intellij.psi.util.PsiTreeUtil;
import com.intellij.psi.util.PsiUtil;
import com.intellij.psi.util.PsiUtilCore;
@@ -114,10 +115,15 @@ public final class AddOnDemandStaticImportAction extends PsiUpdateModCommandActi
@Override
protected @Nullable Presentation getPresentation(@NotNull ActionContext context, @NotNull PsiIdentifier element) {
PsiClass classToImport = getClassToPerformStaticImport(element);
if (classToImport != null) {
return Presentation.of(JavaBundle.message("intention.add.on.demand.static.import.text", classToImport.getQualifiedName()));
if (classToImport == null) {
return null;
}
return null;
Project project = element.getProject();
JavaCodeStyleManager codeStyleManager = JavaCodeStyleManager.getInstance(project);
if (codeStyleManager.isStaticAutoImportClass(classToImport.getQualifiedName())) {
return null;
}
return Presentation.of(JavaBundle.message("intention.add.on.demand.static.import.text", classToImport.getQualifiedName()));
}
public static boolean invoke(final Project project, PsiFile file, final Editor editor, @NotNull PsiElement element) {
@@ -0,0 +1,103 @@
// 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.codeInspection;
import com.intellij.codeInsight.JavaProjectCodeInsightSettings;
import com.intellij.codeInsight.intention.impl.AddOnDemandStaticImportAction;
import com.intellij.java.JavaBundle;
import com.intellij.modcommand.ModPsiUpdater;
import com.intellij.modcommand.PsiUpdateModCommandQuickFix;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.psi.*;
import com.intellij.psi.codeStyle.JavaCodeStyleManager;
import com.intellij.psi.util.PsiTreeUtil;
import com.intellij.util.ObjectUtils;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
public final class StaticImportCanBeUsedInspection extends AbstractBaseJavaLocalInspectionTool implements CleanupLocalInspectionTool {
@Override
public @NotNull PsiElementVisitor buildVisitor(@NotNull ProblemsHolder holder,
boolean isOnTheFly,
@NotNull LocalInspectionToolSession session) {
JavaProjectCodeInsightSettings settings = JavaProjectCodeInsightSettings.getSettings(holder.getProject());
List<String> names = settings.getAllIncludedAutoStaticNames();
if (names.isEmpty()) {
return PsiElementVisitor.EMPTY_VISITOR;
}
Set<String> shortNames = names.stream()
.map(name -> StringUtil.getShortName(name))
.collect(Collectors.toSet());
return new JavaElementVisitor() {
@Override
public void visitReferenceExpression(@NotNull PsiReferenceExpression expression) {
PsiElement reference = expression.getReferenceNameElement();
if (reference == null) return;
PsiExpression qualifierExpression = expression.getQualifierExpression();
if (!(qualifierExpression instanceof PsiReferenceExpression qualifierReferenceExpression)) return;
String name = qualifierReferenceExpression.getReferenceName();
PsiElement qualifierReference = qualifierReferenceExpression.getReferenceNameElement();
if (qualifierReference == null || name == null || !shortNames.contains(name)) return;
OnDemandStaticImportContext context = findOnDemandImportContext(expression);
if (context == null) return;
holder.registerProblem(qualifierReference,
JavaBundle.message("inspection.static.import.can.be.used.display.name"),
new OnDemandStaticImportFix());
}
};
}
private static class OnDemandStaticImportFix extends PsiUpdateModCommandQuickFix {
@Override
protected void applyFix(@NotNull Project project, @NotNull PsiElement element, @NotNull ModPsiUpdater updater) {
PsiElement parent = element.getParent();
if (!(parent instanceof PsiReferenceExpression referenceExpression)) {
return;
}
if (!(referenceExpression.getParent() instanceof PsiReferenceExpression targetReferenceExpression)) return;
JavaCodeStyleManager.getInstance(project).shortenClassReferences(targetReferenceExpression);
}
@Override
public @NotNull String getFamilyName() {
return JavaBundle.message("inspection.static.import.can.be.used.fix.name");
}
}
/**
* @param reference the reference expression to evaluate; must not be null.
* @return the {@link OnDemandStaticImportContext} if the reference can be on-demand static imported,
* or null if the criteria for such a context are not met.
*/
@Nullable
public static OnDemandStaticImportContext findOnDemandImportContext(@NotNull PsiReferenceExpression reference) {
JavaCodeStyleManager javaCodeStyleManager = JavaCodeStyleManager.getInstance(reference.getProject());
PsiFile file = reference.getContainingFile();
if (!(file instanceof PsiJavaFile javaFile)) return null;
if (reference instanceof PsiMethodReferenceExpression) return null;
String memberName = reference.getReferenceName();
if (memberName == null) return null;
PsiJavaCodeReferenceElement qualifier = ObjectUtils.tryCast(reference.getQualifier(), PsiJavaCodeReferenceElement.class);
if (qualifier == null) return null;
if (GenericsUtil.isGenericReference(reference, qualifier)) return null;
if (PsiTreeUtil.getParentOfType(reference, PsiImportStatementBase.class) != null) return null;
if (!(qualifier.resolve() instanceof PsiClass psiClass)) return null;
String classQualifiedName = psiClass.getQualifiedName();
if (!javaCodeStyleManager.isStaticAutoImportClass(classQualifiedName)) return null;
PsiElement referenceNameElement = qualifier.getReferenceNameElement();
if (referenceNameElement == null) return null;
PsiClass anImport = AddOnDemandStaticImportAction.getClassToPerformStaticImport(referenceNameElement);
if (anImport == null) return null;
if (javaCodeStyleManager.hasConflictingOnDemandImport(javaFile, anImport, memberName)) return null;
PsiImportList importList = javaFile.getImportList();
if (importList == null) return null;
return new OnDemandStaticImportContext(anImport, memberName, importList);
}
public record OnDemandStaticImportContext(@NotNull PsiClass psiClass, @NotNull String memberName, @NotNull PsiImportList importList) {
}
}
@@ -1004,6 +1004,43 @@ public final class ImportHelper {
return true;
}
}
JavaPsiFacade facade = JavaPsiFacade.getInstance(file.getProject());
PsiResolveHelper resolveHelper = facade.getResolveHelper();
for (Import anImport : resultList) {
if (anImport.isStatic()) {
String shortName = StringUtil.getShortName(anImport.name());
String prefix = StringUtil.getPackageName(anImport.name());
if (prefix.isEmpty()) continue;
PsiField field = psiClass.findFieldByName(shortName, true);
if (field != null &&
field.hasModifierProperty(PsiModifier.STATIC) &&
checkMemberAccessibility(field, resolveHelper, file, psiClass, prefix)) {
if (isOnDemandStaticImported(file, anImport)) {
return true;
}
}
else {
PsiClass inner = psiClass.findInnerClassByName(shortName, true);
if (inner != null &&
inner.hasModifierProperty(PsiModifier.STATIC) &&
checkMemberAccessibility(inner, resolveHelper, file, psiClass, prefix)) {
if (isOnDemandStaticImported(file, anImport)) {
return true;
}
}
else {
PsiMethod[] methods = psiClass.findMethodsByName(shortName, true);
if (ContainerUtil.exists(methods, psiMethod ->
psiMethod.hasModifierProperty(PsiModifier.STATIC) &&
checkMemberAccessibility(psiMethod, resolveHelper, file, psiClass, prefix))) {
if (isOnDemandStaticImported(file, anImport)) {
return true;
}
}
}
}
}
}
PsiImportList importList = file.getImportList();
if (importList == null) return false;
@@ -1022,6 +1059,21 @@ public final class ImportHelper {
return singleImports.contains(newImport);
}
private static boolean isOnDemandStaticImported(@NotNull PsiJavaFile file, @NotNull Import anImport) {
if(!anImport.isStatic()) return false;
PsiImportList importList = file.getImportList();
if(importList==null)return false;
for (PsiImportStaticStatement statement : importList.getImportStaticStatements()) {
if(!statement.isOnDemand()) return false;
String packageName = StringUtil.getPackageName(anImport.name);
if (statement.getImportReference() != null &&
packageName.equals(statement.getImportReference().getQualifiedName())) {
return true;
}
}
return false;
}
// returns list of (name, isImportStatic) pairs
private static @NotNull Collection<Import> collectNamesToImport(@NotNull PsiJavaFile file, @NotNull List<? super PsiElement> comments) {
Set<Import> imports = new HashSet<>();
@@ -4,12 +4,11 @@ package com.intellij.psi.impl.source.codeStyle;
import com.intellij.application.options.CodeStyle;
import com.intellij.codeInsight.AnnotationTargetUtil;
import com.intellij.codeInsight.daemon.impl.analysis.JavaModuleGraphUtil;
import com.intellij.codeInsight.intention.impl.AddOnDemandStaticImportAction;
import com.intellij.codeInspection.StaticImportCanBeUsedInspection;
import com.intellij.lang.ASTNode;
import com.intellij.openapi.project.Project;
import com.intellij.psi.*;
import com.intellij.psi.codeStyle.CodeStyleSettings;
import com.intellij.psi.codeStyle.JavaCodeStyleManager;
import com.intellij.psi.codeStyle.JavaCodeStyleSettings;
import com.intellij.psi.codeStyle.ReferenceAdjuster;
import com.intellij.psi.impl.PsiImplUtil;
@@ -22,7 +21,6 @@ import com.intellij.psi.jsp.JspFile;
import com.intellij.psi.tree.IElementType;
import com.intellij.psi.util.PsiTreeUtil;
import com.intellij.psi.util.PsiUtil;
import com.intellij.util.ObjectUtils;
import com.siyeh.ig.psiutils.CommentTracker;
import com.siyeh.ig.psiutils.ImportUtils;
import one.util.streamex.StreamEx;
@@ -139,27 +137,19 @@ public final class JavaReferenceAdjuster implements ReferenceAdjuster {
}
private static boolean tryAutoStaticallyImport(@NotNull PsiReferenceExpression reference) {
JavaCodeStyleManager javaCodeStyleManager = JavaCodeStyleManager.getInstance(reference.getProject());
PsiFile file = reference.getContainingFile();
if (!(file instanceof PsiJavaFile javaFile)) return false;
if (reference instanceof PsiMethodReferenceExpression) return false;
String memberName = reference.getReferenceName();
if (memberName == null) return false;
PsiJavaCodeReferenceElement qualifier = ObjectUtils.tryCast(reference.getQualifier(), PsiJavaCodeReferenceElement.class);
if (qualifier == null) return false;
if (GenericsUtil.isGenericReference(reference, qualifier)) return false;
if (PsiTreeUtil.getParentOfType(reference, PsiImportStatementBase.class) != null) return false;
if (!(qualifier.resolve() instanceof PsiClass psiClass)) return false;
String classQualifiedName = psiClass.getQualifiedName();
if (!javaCodeStyleManager.isStaticAutoImportClass(classQualifiedName)) return false;
PsiElement referenceNameElement = qualifier.getReferenceNameElement();
if (referenceNameElement == null) return false;
PsiClass anImport = AddOnDemandStaticImportAction.getClassToPerformStaticImport(referenceNameElement);
if (anImport == null) return false;
if (javaCodeStyleManager.hasConflictingOnDemandImport(javaFile, anImport, memberName)) return false;
PsiImportList importList = javaFile.getImportList();
if (importList == null) return false;
PsiReferenceExpressionImpl.bindToElementViaStaticImport(anImport, memberName, importList);
StaticImportCanBeUsedInspection.OnDemandStaticImportContext context =
StaticImportCanBeUsedInspection.findOnDemandImportContext(reference);
if (context == null) return false;
PsiClass qualifierClass = context.psiClass();
String qualifiedName = qualifierClass.getQualifiedName();
List<PsiJavaCodeReferenceElement> refs = PsiReferenceExpressionImpl.getImportsFromClass(context.importList(), qualifiedName);
for (PsiJavaCodeReferenceElement ref : refs) {
PsiImportStaticStatement importStatement = PsiTreeUtil.getParentOfType(ref, PsiImportStaticStatement.class);
if (importStatement != null) {
importStatement.delete();
}
}
context.importList().add(JavaPsiFacade.getElementFactory(qualifierClass.getProject()).createImportStaticStatement(qualifierClass, "*"));
return true;
}
@@ -0,0 +1,10 @@
<html>
<body>
Reports qualifiers, which can be replaced with <code>on-demand import static</code> statements.
The list of classes that will be checked can be configured in
<a href="settings://editor.preferences.import">Settings | Editor | General | Auto Import | Java | Include auto-import of static members in
completion</a>
</body>
<!-- tooltip end -->
<p><small>New in 2025.1</small></p>
</html>
@@ -122,7 +122,15 @@ public class PsiReferenceExpressionImpl extends ExpressionPsiElement implements
}
}
private static @NotNull List<PsiJavaCodeReferenceElement> getImportsFromClass(@NotNull PsiImportList importList, String className) {
/**
* Retrieves the static import statements from the given import list that reference
* a specified class.
*
* @param importList the list of import statements in a Java file.
* @param className the fully qualified name of the class for which static imports are to be retrieved.
* @return a list of static import references corresponding to the specified class.
*/
public static @NotNull List<PsiJavaCodeReferenceElement> getImportsFromClass(@NotNull PsiImportList importList, String className) {
List<PsiJavaCodeReferenceElement> array = new ArrayList<>();
for (PsiImportStaticStatement staticStatement : importList.getImportStaticStatements()) {
PsiClass psiClass = staticStatement.resolveTargetClass();
@@ -0,0 +1,11 @@
package staticImportCanBeUsed;
import java.util.Arrays;
import static java.util.Arrays.sort;
class Foo {
void test(String[] baz) {
<warning descr="On-demand static import can be used">Arrays</warning><caret>.sort(baz);
<warning descr="On-demand static import can be used">Arrays</warning>.sort(baz);
}
}
@@ -0,0 +1,11 @@
package staticImportCanBeUsed;
import java.util.Arrays;
import static java.util.Arrays.sort;
class Foo {
void test(String[] baz) {
<caret>sort(baz);
sort(baz);
}
}
@@ -0,0 +1,10 @@
package staticImportCanBeUsed;
import java.util.Arrays;
class Foo {
void test(String[] baz) {
<warning descr="On-demand static import can be used">Arrays</warning><caret>.sort(baz);
<warning descr="On-demand static import can be used">Arrays</warning>.sort(baz);
}
}
@@ -0,0 +1,12 @@
package staticImportCanBeUsed;
import java.util.Arrays;
import static java.util.Arrays.*;
class Foo {
void test(String[] baz) {
<caret>sort(baz);
sort(baz);
}
}
@@ -0,0 +1,12 @@
package staticImportCanBeUsed;
import java.util.Arrays;
class Foo {
void test(String[] baz) {
Arrays.sort(baz);
Arrays.sort(baz);
}
public static void sort(String[] a) {}
}
@@ -0,0 +1,13 @@
package staticImportCanBeUsed;
import java.util.Arrays;
import static org.Foo2.*;
class Foo {
void test(String[] baz) {
Arrays.sort(baz);
Arrays.sort(baz);
binarySearch(baz, "a");
}
}
@@ -0,0 +1,13 @@
package staticImportCanBeUsed;
import org.Foo2;
import static org.Foo3.*;
class Foo {
void test(String[] baz) {
Foo2.sort(baz);
Foo2.sort(baz);
new Calculus();
}
}
@@ -0,0 +1,13 @@
package staticImportCanBeUsed;
import org.Foo2;
import static org.Foo3.*;
class Foo {
void test(String[] baz) {
Foo2.sort(baz);
Foo2.sort(baz);
System.out.println(PI);
}
}
@@ -0,0 +1,122 @@
// 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.JavaTestUtil;
import com.intellij.analysis.AnalysisBundle;
import com.intellij.codeInsight.JavaProjectCodeInsightSettings;
import com.intellij.codeInsight.intention.IntentionAction;
import com.intellij.codeInspection.InspectionProfileEntry;
import com.intellij.codeInspection.StaticImportCanBeUsedInspection;
import com.siyeh.ig.LightJavaInspectionTestCase;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
public class StaticImportCanBeUsedInspectionTest extends LightJavaInspectionTestCase {
@Override
public void tearDown() throws Exception {
try {
JavaProjectCodeInsightSettings.getSettings(getProject()).includedAutoStaticNames.clear();
}
catch (Throwable e) {
addSuppressedException(e);
}
finally {
super.tearDown();
}
}
@Nullable
@Override
protected InspectionProfileEntry getInspection() {
return new StaticImportCanBeUsedInspection();
}
public void testSimple() {
addStaticAutoImport("java.util.Arrays");
doTest();
cleanupTest();
}
public void testWithConflicts() {
addStaticAutoImport("java.util.Arrays");
doTest();
}
public void testWithConflicts2() {
addStaticAutoImport("java.util.Arrays");
myFixture.addClass("""
package org;
public final class Foo2 {
public static void binarySearch(Object[] args, Object key) {}
}
""");
doTest();
}
public void testWithConflictsWithField() {
addStaticAutoImport("org.Foo2");
myFixture.addClass("""
package org;
public final class Foo2 {
public static final String PI = "3.14"
public static void sort(String[] args) {}
}
""");
myFixture.addClass("""
package org;
public final class Foo3 {
public static final String PI = "3.14"
}
""");
doTest();
}
public void testWithConflictsWithClass() {
addStaticAutoImport("org.Foo2");
myFixture.addClass("""
package org;
public final class Foo2 {
public static class Calculus{}
public static void sort(String[] args) {}
}
""");
myFixture.addClass("""
package org;
public final class Foo3 {
public static class Calculus{}
}
""");
doTest();
}
public void testAlreadyImported() {
addStaticAutoImport("java.util.Arrays");
doTest();
cleanupTest();
}
//todo test for inner class and for field
private void cleanupTest() {
IntentionAction intention = myFixture.getAvailableIntention(AnalysisBundle.message("cleanup.in.file"));
myFixture.launchAction(intention);
myFixture.checkResultByFile(getTestName(false) + "_after.java");
}
@Override
protected String getBasePath() {
return JavaTestUtil.getRelativeJavaTestDataPath() + "/inspection/staticImportCanBeUsed/";
}
@SuppressWarnings("SameParameterValue")
private void addStaticAutoImport(@NotNull String name) {
JavaProjectCodeInsightSettings.getSettings(getProject()).includedAutoStaticNames.add(name);
}
}
@@ -1429,6 +1429,8 @@ inspection.slow.abstract.set.remove.all.fix.family.name=Use 'Set.remove' instead
inspection.slow.list.contains.all.description=Call to 'list.containsAll(collection)' may have poor performance
inspection.slow.list.contains.all.fix.family.name=Wrap in 'HashSet' constructor
inspection.slow.list.contains.all.fix.name=Wrap ''{0}'' in ''HashSet'' constructor
inspection.static.import.can.be.used.display.name=On-demand static import can be used
inspection.static.import.can.be.used.fix.name=Add on-demand static import
slice.filter.parse.error.null.filter.not.applicable.for.primitive.type=''null'' filter is not applicable for primitive type {0}
slice.filter.parse.error.not.null.filter.not.applicable.for.primitive.type=''!null'' filter is not applicable for primitive type {0}
slice.filter.parse.error.enum.constant.not.found=Enum constant not found: {0}