implement SuperClassHasFrequentlyUsedInheritors inspection over compiler indices

This commit is contained in:
Dmitry Batkovich
2017-05-02 18:26:19 +03:00
parent 58d04ad86f
commit 283dfa0957
18 changed files with 367 additions and 487 deletions
@@ -62,7 +62,7 @@ class CompilerReferenceReader {
}
else {
LightRef.LightClassHierarchyElementDef hierarchyElement = ((LightRef.LightMember)ref).getOwner();
hierarchy = getWholeHierarchy(hierarchyElement, checkBaseClassAmbiguity, -1);
hierarchy = getHierarchy(hierarchyElement, checkBaseClassAmbiguity, false, -1);
}
if (hierarchy == null) return null;
TIntHashSet set = new TIntHashSet();
@@ -222,25 +222,28 @@ class CompilerReferenceReader {
}
@Nullable("return null if the class hierarchy contains ambiguous qualified names")
LightRef.NamedLightRef[] getWholeHierarchy(LightRef.LightClassHierarchyElementDef hierarchyElement, boolean checkBaseClassAmbiguity, int interruptNumber) {
LightRef.LightClassHierarchyElementDef[] getHierarchy(LightRef.LightClassHierarchyElementDef hierarchyElement,
boolean checkBaseClassAmbiguity,
boolean includeAnonymous,
int interruptNumber) {
try {
Set<LightRef.NamedLightRef> result = new THashSet<>();
Queue<LightRef.NamedLightRef> q = new Queue<>(10);
Set<LightRef.LightClassHierarchyElementDef> result = new THashSet<>();
Queue<LightRef.LightClassHierarchyElementDef> q = new Queue<>(10);
q.addLast(hierarchyElement);
while (!q.isEmpty()) {
LightRef.NamedLightRef curClass = q.pullFirst();
LightRef.LightClassHierarchyElementDef curClass = q.pullFirst();
if (interruptNumber != -1 && result.size() > interruptNumber) {
break;
}
if (result.add(curClass)) {
if (checkBaseClassAmbiguity || curClass != hierarchyElement) {
if (!(curClass instanceof LightRef.LightAnonymousClassDef) && (checkBaseClassAmbiguity || curClass != hierarchyElement)) {
if (hasMultipleDefinitions(curClass)) {
return null;
}
}
myIndex.get(CompilerIndices.BACK_HIERARCHY).getData(curClass).forEach((id, children) -> {
for (LightRef child : children) {
if (child instanceof LightRef.LightClassHierarchyElementDef && !(child instanceof LightRef.LightAnonymousClassDef)) {
if (child instanceof LightRef.LightClassHierarchyElementDef && (includeAnonymous || !(child instanceof LightRef.LightAnonymousClassDef))) {
q.addLast((LightRef.LightClassHierarchyElementDef)child);
}
}
@@ -248,13 +251,27 @@ class CompilerReferenceReader {
});
}
}
return result.toArray(LightRef.NamedLightRef.EMPTY_ARRAY);
return result.toArray(LightRef.LightClassHierarchyElementDef.EMPTY_ARRAY);
}
catch (StorageException e) {
throw new RuntimeException(e);
}
}
@NotNull
LightRef.LightClassHierarchyElementDef[] getDirectInheritors(LightRef.LightClassHierarchyElementDef hierarchyElement) throws StorageException {
Set<LightRef.LightClassHierarchyElementDef> result = new THashSet<>();
myIndex.get(CompilerIndices.BACK_HIERARCHY).getData(hierarchyElement).forEach((id, children) -> {
for (LightRef child : children) {
if (child instanceof LightRef.LightClassHierarchyElementDef && !(child instanceof LightRef.LightAnonymousClassDef)) {
result.add((LightRef.LightClassHierarchyElementDef)child);
}
}
return true;
});
return result.toArray(LightRef.LightClassHierarchyElementDef.EMPTY_ARRAY);
}
private enum DefCount { NONE, ONE, MANY}
private boolean hasMultipleDefinitions(LightRef.NamedLightRef def) throws StorageException {
DefCount[] result = new DefCount[]{DefCount.NONE};
@@ -25,7 +25,7 @@ import org.jetbrains.jps.backwardRefs.SignatureData;
import java.util.SortedSet;
/**
* The service is used for java relevant chain completion
* The service is used for java relevant chain completion / frequently used superclass inspection
*/
public abstract class CompilerReferenceServiceEx extends CompilerReferenceService {
protected CompilerReferenceServiceEx(Project project) {
@@ -43,4 +43,11 @@ public abstract class CompilerReferenceServiceEx extends CompilerReferenceServic
@NotNull
public abstract String getName(int idx)
throws ReferenceIndexUnavailableException;
public abstract int getNameId(@NotNull String name) throws ReferenceIndexUnavailableException;
@NotNull
public abstract LightRef.LightClassHierarchyElementDef[] getDirectInheritors(LightRef.LightClassHierarchyElementDef baseClass) throws ReferenceIndexUnavailableException;
public abstract int getInheritorCount(LightRef.LightClassHierarchyElementDef baseClass) throws ReferenceIndexUnavailableException;
}
@@ -53,6 +53,7 @@ import com.intellij.psi.util.CachedValueProvider;
import com.intellij.psi.util.CachedValuesManager;
import com.intellij.psi.util.PsiModificationTracker;
import com.intellij.psi.util.PsiUtilCore;
import com.intellij.util.ObjectUtils;
import com.intellij.util.containers.ConcurrentFactoryMap;
import com.intellij.util.indexing.FileBasedIndex;
import com.intellij.util.indexing.StorageException;
@@ -235,7 +236,7 @@ public class CompilerReferenceServiceImpl extends CompilerReferenceServiceEx imp
.filter(r -> r instanceof LightRef.JavaLightMethodRef)
.map(r -> (LightRef.JavaLightMethodRef) r)
.flatMap(r -> {
LightRef.NamedLightRef[] hierarchy = myReader.getWholeHierarchy(r.getOwner(), false, ChainSearchMagicConstants.MAX_HIERARCHY_SIZE);
LightRef.NamedLightRef[] hierarchy = myReader.getHierarchy(r.getOwner(), false, false, ChainSearchMagicConstants.MAX_HIERARCHY_SIZE);
return hierarchy == null ? Stream.empty() : Arrays.stream(hierarchy).map(c -> r.override(c.getName()));
})
.distinct()
@@ -278,7 +279,7 @@ public class CompilerReferenceServiceImpl extends CompilerReferenceServiceEx imp
return false;
}
catch (Exception e) {
onException(e, "correlation");
onException(e, "conditional probability");
return false;
}
} finally {
@@ -293,6 +294,58 @@ public class CompilerReferenceServiceImpl extends CompilerReferenceServiceEx imp
try {
if (myReader == null) throw new ReferenceIndexUnavailableException();
return myReader.getNameEnumerator().getName(idx);
} catch (Exception e) {
onException(e, "find methods");
throw new ReferenceIndexUnavailableException();
} finally {
myReadDataLock.unlock();
}
}
@Override
public int getNameId(@NotNull String name) throws ReferenceIndexUnavailableException {
myReadDataLock.lock();
try {
if (myReader == null) throw new ReferenceIndexUnavailableException();
int id;
try {
id = myReader.getNameEnumerator().tryEnumerate(name);
}
catch (Exception e) {
onException(e, "get name-id");
throw new ReferenceIndexUnavailableException();
}
return id;
} finally {
myReadDataLock.unlock();
}
}
@NotNull
@Override
public LightRef.LightClassHierarchyElementDef[] getDirectInheritors(@NotNull LightRef.LightClassHierarchyElementDef baseClass) throws ReferenceIndexUnavailableException {
myReadDataLock.lock();
try {
if (myReader == null) throw new ReferenceIndexUnavailableException();
return myReader.getDirectInheritors(baseClass);
} catch (Exception e) {
onException(e, "find methods");
throw new ReferenceIndexUnavailableException();
} finally {
myReadDataLock.unlock();
}
}
@Override
public int getInheritorCount(@NotNull LightRef.LightClassHierarchyElementDef baseClass) throws ReferenceIndexUnavailableException {
myReadDataLock.lock();
try {
if (myReader == null) throw new ReferenceIndexUnavailableException();
LightRef.NamedLightRef[] hierarchy = ObjectUtils.notNull(myReader.getHierarchy(baseClass, false, true, -1));
return hierarchy.length;
} catch (Exception e) {
onException(e, "inheritor count");
throw new ReferenceIndexUnavailableException();
} finally {
myReadDataLock.unlock();
}
@@ -1,5 +1,5 @@
/*
* Copyright 2000-2013 JetBrains s.r.o.
* Copyright 2000-2017 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -13,34 +13,37 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.intellij.codeInspection.inheritance;
package com.intellij.compiler.inspection;
import com.intellij.codeInsight.daemon.GroupNames;
import com.intellij.codeInsight.intention.LowPriorityAction;
import com.intellij.codeInsight.intention.HighPriorityAction;
import com.intellij.codeInspection.LocalQuickFix;
import com.intellij.codeInspection.ProblemDescriptor;
import com.intellij.openapi.project.Project;
import com.intellij.psi.*;
import com.intellij.psi.codeStyle.JavaCodeStyleManager;
import com.intellij.util.ArrayUtil;
import com.intellij.util.ObjectUtils;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.TestOnly;
public class ChangeSuperClassFix implements LocalQuickFix {
public class ChangeSuperClassFix implements LocalQuickFix, HighPriorityAction {
@NotNull
private final SmartPsiElementPointer<PsiClass> myNewSuperClass;
@NotNull
private final SmartPsiElementPointer<PsiClass> myOldSuperClass;
private final int myPercent;
private final int myInheritorCount;
@NotNull
private final String myNewSuperName;
private final boolean myNewSuperIsInterface;
public ChangeSuperClassFix(@NotNull final PsiClass newSuperClass, final int percent, @NotNull final PsiClass oldSuperClass) {
final SmartPointerManager smartPointerManager = SmartPointerManager.getInstance(newSuperClass.getProject());
myNewSuperName = ObjectUtils.notNull(newSuperClass.getQualifiedName());
myNewSuperIsInterface = newSuperClass.isInterface();
myNewSuperClass = smartPointerManager.createSmartPsiElementPointer(newSuperClass);
myOldSuperClass = smartPointerManager.createSmartPsiElementPointer(oldSuperClass);
myPercent = percent;
myInheritorCount = percent;
}
@NotNull
@@ -50,14 +53,14 @@ public class ChangeSuperClassFix implements LocalQuickFix {
}
@TestOnly
public int getPercent() {
return myPercent;
public int getInheritorCount() {
return myInheritorCount;
}
@NotNull
@Override
public String getName() {
return String.format("Make extends '%s' - %s%%", myNewSuperName, myPercent);
return String.format("Make " + (myNewSuperIsInterface ? "implements" : "extends") + " '%s'", myNewSuperName);
}
@NotNull
@@ -83,42 +86,38 @@ public class ChangeSuperClassFix implements LocalQuickFix {
private static void changeSuperClass(@NotNull final PsiClass aClass,
@NotNull final PsiClass oldSuperClass,
@NotNull final PsiClass newSuperClass) {
PsiElementFactory factory = JavaPsiFacade.getInstance(aClass.getProject()).getElementFactory();
JavaPsiFacade psiFacade = JavaPsiFacade.getInstance(aClass.getProject());
PsiElementFactory factory = psiFacade.getElementFactory();
if (aClass instanceof PsiAnonymousClass) {
((PsiAnonymousClass)aClass).getBaseClassReference().replace(factory.createClassReferenceElement(newSuperClass));
return;
}
else if (oldSuperClass.isInterface()) {
final PsiReferenceList interfaceList = aClass.getImplementsList();
if (interfaceList != null) {
for (final PsiJavaCodeReferenceElement interfaceRef : interfaceList.getReferenceElements()) {
final PsiElement aInterface = interfaceRef.resolve();
if (aInterface != null && aInterface.isEquivalentTo(oldSuperClass)) {
interfaceRef.delete();
}
}
PsiReferenceList extendsList = ObjectUtils.notNull(aClass.getExtendsList());
PsiJavaCodeReferenceElement[] refElements =
ArrayUtil.mergeArrays(getReferences(extendsList), getReferences(aClass.getImplementsList()));
for (PsiJavaCodeReferenceElement refElement : refElements) {
if (refElement.isReferenceTo(oldSuperClass)) {
refElement.delete();
}
}
final PsiReferenceList extendsList = aClass.getExtendsList();
if (extendsList != null) {
final PsiJavaCodeReferenceElement newClassReference = factory.createClassReferenceElement(newSuperClass);
if (extendsList.getReferenceElements().length == 0) {
extendsList.add(newClassReference);
}
}
PsiReferenceList list;
if (newSuperClass.isInterface()) {
list = aClass.getImplementsList();
}
else {
final PsiReferenceList extendsList = aClass.getExtendsList();
if (extendsList != null && extendsList.getReferenceElements().length == 1) {
extendsList.getReferenceElements()[0].delete();
PsiElement ref = extendsList.add(factory.createClassReferenceElement(newSuperClass));
JavaCodeStyleManager.getInstance(aClass.getProject()).shortenClassReferences(ref);
list = extendsList;
PsiJavaCodeReferenceElement[] elements = list.getReferenceElements();
if (elements.length == 1 &&
elements[0].isReferenceTo(psiFacade.findClass(CommonClassNames.JAVA_LANG_OBJECT, aClass.getResolveScope()))) {
elements[0].delete();
}
}
PsiElement ref = list.add(factory.createClassReferenceElement(newSuperClass));
JavaCodeStyleManager.getInstance(aClass.getProject()).shortenClassReferences(ref);
}
public static class LowPriority extends ChangeSuperClassFix implements LowPriorityAction {
public LowPriority(@NotNull final PsiClass newSuperClass, final int percent, @NotNull final PsiClass oldSuperClass) {
super(newSuperClass, percent, oldSuperClass);
}
private static PsiJavaCodeReferenceElement[] getReferences(PsiReferenceList list) {
return list == null ? PsiJavaCodeReferenceElement.EMPTY_ARRAY : list.getReferenceElements();
}
}
@@ -0,0 +1,210 @@
/*
* Copyright 2000-2017 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.intellij.compiler.inspection;
import com.intellij.codeInspection.*;
import com.intellij.compiler.CompilerReferenceService;
import com.intellij.compiler.backwardRefs.CompilerReferenceServiceEx;
import com.intellij.compiler.backwardRefs.ReferenceIndexUnavailableException;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.roots.ProjectFileIndex;
import com.intellij.openapi.roots.ProjectRootManager;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.psi.*;
import com.intellij.psi.impl.java.stubs.index.JavaFullClassNameIndex;
import com.intellij.psi.search.GlobalSearchScope;
import one.util.streamex.MoreCollectors;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.jps.backwardRefs.LightRef;
import java.util.*;
import java.util.stream.Collectors;
import java.util.stream.Stream;
public class FrequentlyUsedInheritorInspection extends BaseJavaLocalInspectionTool {
private static final Logger LOG = Logger.getInstance(FrequentlyUsedInheritorInspection.class);
public static final byte MAX_RESULT = 3;
private static final int PERCENT_THRESHOLD = 20;
@Nullable
@Override
public ProblemDescriptor[] checkClass(@NotNull final PsiClass aClass,
@NotNull final InspectionManager manager,
final boolean isOnTheFly) {
if (aClass.isInterface() || aClass instanceof PsiTypeParameter) {
return null;
}
final PsiClass superClass = getSuperIfOnlyOne(aClass);
if (superClass == null) return null;
long ms = System.currentTimeMillis();
final List<ClassAndInheritorCount> topInheritors = getTopInheritorsUsingCompilerIndices(superClass, aClass.getResolveScope(), aClass);
if (LOG.isDebugEnabled()) {
LOG.debug("search for inheritance structure of " + superClass.getQualifiedName() + " in " + (System.currentTimeMillis() - ms) + " ms");
}
if (topInheritors.isEmpty()) return null;
final Collection<LocalQuickFix> topInheritorsQuickFix = new ArrayList<>(topInheritors.size());
for (final ClassAndInheritorCount searchResult : topInheritors) {
final LocalQuickFix quickFix = new ChangeSuperClassFix(searchResult.psi, searchResult.number, superClass);
topInheritorsQuickFix.add(quickFix);
if (topInheritorsQuickFix.size() >= MAX_RESULT) {
break;
}
}
return new ProblemDescriptor[]{manager
.createProblemDescriptor(aClass, "Class can have more common super class", isOnTheFly,
topInheritorsQuickFix.toArray(LocalQuickFix.EMPTY_ARRAY),
ProblemHighlightType.GENERIC_ERROR_OR_WARNING)};
}
@Nullable
private static PsiClass getSuperIfOnlyOne(@NotNull final PsiClass aClass) {
PsiClass superClass = aClass.getSuperClass();
if (superClass != null && !CommonClassNames.JAVA_LANG_OBJECT.equals(superClass.getQualifiedName())) {
return isInSourceContent(aClass) ? superClass : null;
}
return Arrays
.stream(aClass.getInterfaces())
.filter(c -> !CommonClassNames.JAVA_LANG_OBJECT.equals(c.getQualifiedName()))
.filter(c -> isInSourceContent(c))
.collect(MoreCollectors.onlyOne())
.orElse(null);
}
@NotNull
private static List<ClassAndInheritorCount> getTopInheritorsUsingCompilerIndices(@NotNull PsiClass aClass,
@NotNull GlobalSearchScope searchScope,
@NotNull PsiElement place) {
String qName = aClass.getQualifiedName();
if (qName == null) return Collections.emptyList();
final Project project = aClass.getProject();
final CompilerReferenceServiceEx compilerRefService = (CompilerReferenceServiceEx)CompilerReferenceService.getInstance(project);
try {
int id = compilerRefService.getNameId(qName);
if (id == 0) return Collections.emptyList();
return findInheritors(aClass, new LightRef.JavaLightClassRef(id), searchScope, place, -1, project, compilerRefService);
}
catch (ReferenceIndexUnavailableException e) {
return Collections.emptyList();
}
}
private static List<ClassAndInheritorCount> findInheritors(@NotNull PsiClass aClass,
@NotNull LightRef.JavaLightClassRef classAsLightRef,
@NotNull GlobalSearchScope searchScope,
@NotNull PsiElement place,
int hierarchyCardinality,
@NotNull Project project,
@NotNull CompilerReferenceServiceEx compilerRefService) {
LightRef.LightClassHierarchyElementDef[] directInheritors = compilerRefService.getDirectInheritors(classAsLightRef);
if (hierarchyCardinality == -1) {
hierarchyCardinality = compilerRefService.getInheritorCount(classAsLightRef);
}
int finalHierarchyCardinality = hierarchyCardinality;
List<ClassAndInheritorCount> directInheritorStats = Stream
.of(directInheritors)
.filter(inheritor -> !(inheritor instanceof LightRef.LightAnonymousClassDef))
.map(inheritor -> {
int count = compilerRefService.getInheritorCount(inheritor);
if (count * 100 > finalHierarchyCardinality * PERCENT_THRESHOLD) {
return new Object() {
final LightRef.LightClassHierarchyElementDef myDef = inheritor;
final int inheritorCount = count;
};
}
return null;
})
.filter(Objects::nonNull)
.map(defAndCount -> {
String name = compilerRefService.getName(defAndCount.myDef.getName());
PsiClass inheritor =
JavaFullClassNameIndex.getInstance().get(name.hashCode(), project, searchScope).stream()
.filter(cls -> name.equals(cls.getQualifiedName()))
.collect(MoreCollectors.onlyOne())
.orElse(null);
if (inheritor == null || !inheritor.isInheritor(aClass, false)) {
return null;
}
return new ClassAndInheritorCount(inheritor, defAndCount.myDef, defAndCount.inheritorCount);
})
.filter(Objects::nonNull)
.collect(Collectors.toList());
PsiResolveHelper resolveHelper = PsiResolveHelper.SERVICE.getInstance(project);
return directInheritorStats
.stream()
.filter(c -> resolveHelper.isAccessible(c.psi, place, null))
.flatMap(c -> Stream.concat(Stream.of(c), getClassesIfInterface(c, finalHierarchyCardinality, searchScope, place, project, compilerRefService).stream()))
.sorted()
.limit(MAX_RESULT)
.collect(Collectors.toList());
}
private static List<ClassAndInheritorCount> getClassesIfInterface(@NotNull ClassAndInheritorCount classAndInheritorCount,
int hierarchyCardinality,
GlobalSearchScope searchScope,
PsiElement place,
Project project,
CompilerReferenceServiceEx compilerRefService) {
if (classAndInheritorCount.psi.isInterface()) {
return findInheritors(classAndInheritorCount.psi,
(LightRef.JavaLightClassRef)classAndInheritorCount.descriptor,
searchScope,
place,
hierarchyCardinality,
project,
compilerRefService);
}
return Collections.emptyList();
}
private static boolean isInSourceContent(@NotNull PsiElement e) {
final VirtualFile file = e.getContainingFile().getVirtualFile();
if (file == null) return false;
final ProjectFileIndex index = ProjectRootManager.getInstance(e.getProject()).getFileIndex();
return index.isInContent(file);
}
private static class ClassAndInheritorCount implements Comparable<ClassAndInheritorCount> {
private final PsiClass psi;
private final LightRef.LightClassHierarchyElementDef descriptor;
private final int number;
private ClassAndInheritorCount(PsiClass psi,
LightRef.LightClassHierarchyElementDef descriptor,
int number) {
this.psi = psi;
this.descriptor = descriptor;
this.number = number;
}
@Override
public int compareTo(@NotNull ClassAndInheritorCount o) {
return - number + o.number;
}
}
}
@@ -1,116 +0,0 @@
package com.intellij.codeInspection.inheritance;
import com.intellij.codeInsight.daemon.GroupNames;
import com.intellij.codeInspection.*;
import com.intellij.codeInspection.inheritance.search.InheritorsStatisticalDataSearch;
import com.intellij.codeInspection.inheritance.search.InheritorsStatisticsSearchResult;
import com.intellij.psi.*;
import org.jetbrains.annotations.Nls;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
public class SuperClassHasFrequentlyUsedInheritorsInspection extends BaseJavaBatchLocalInspectionTool {
private static final int MIN_PERCENT_RATIO = 5;
public static final int MAX_QUICK_FIX_COUNTS = 4;
@Nls
@NotNull
@Override
public String getGroupDisplayName() {
return GroupNames.INHERITANCE_GROUP_NAME;
}
@Nls
@NotNull
@Override
public String getDisplayName() {
return "Class may extend a commonly used base class instead of implementing interface or extending abstract class";
}
@Override
public boolean isEnabledByDefault() {
return false;
}
@Nullable
@Override
public ProblemDescriptor[] checkClass(@NotNull final PsiClass aClass,
@NotNull final InspectionManager manager,
final boolean isOnTheFly) {
if (aClass.isInterface() ||
aClass.isEnum() ||
aClass instanceof PsiTypeParameter ||
aClass.getMethods().length != 0 ||
aClass.hasModifierProperty(PsiModifier.ABSTRACT)) {
return null;
}
final PsiClass superClass = getSuperIfUnique(aClass);
if (superClass == null) return null;
final List<InheritorsStatisticsSearchResult> topInheritors =
InheritorsStatisticalDataSearch.search(superClass, aClass, aClass.getResolveScope(), MIN_PERCENT_RATIO);
if (topInheritors.isEmpty()) {
return null;
}
final Collection<LocalQuickFix> topInheritorsQuickFix = new ArrayList<>(topInheritors.size());
boolean isFirst = true;
for (final InheritorsStatisticsSearchResult searchResult : topInheritors) {
final LocalQuickFix quickFix;
if (isFirst) {
quickFix = new ChangeSuperClassFix(searchResult.getPsiClass(), searchResult.getPercent(), superClass);
isFirst = false;
} else {
quickFix = new ChangeSuperClassFix.LowPriority(searchResult.getPsiClass(), searchResult.getPercent(), superClass);
}
topInheritorsQuickFix.add(quickFix);
if (topInheritorsQuickFix.size() >= MAX_QUICK_FIX_COUNTS) {
break;
}
}
return new ProblemDescriptor[]{
manager.createProblemDescriptor(aClass, getDisplayName(), false, ProblemHighlightType.INFORMATION, false,
topInheritorsQuickFix.toArray(new LocalQuickFix[topInheritorsQuickFix.size()]))};
}
@Nullable
private static PsiClass getSuperIfUnique(@NotNull final PsiClass aClass) {
if (aClass instanceof PsiAnonymousClass) {
final PsiClass returnClass = (PsiClass)((PsiAnonymousClass)aClass).getBaseClassReference().resolve();
if (returnClass != null && CommonClassNames.JAVA_LANG_OBJECT.equals(returnClass.getQualifiedName())) return null;
return returnClass;
}
final PsiReferenceList extendsList = aClass.getExtendsList();
if (extendsList != null) {
final PsiJavaCodeReferenceElement[] referenceElements = extendsList.getReferenceElements();
if (referenceElements.length == 1) {
final PsiElement resolved = referenceElements[0].resolve();
if (resolved instanceof PsiClass) {
PsiClass returnClass = (PsiClass)resolved;
if (!CommonClassNames.JAVA_LANG_OBJECT.equals(returnClass.getQualifiedName()) && !returnClass.isInterface()) {
return returnClass;
}
}
}
}
final PsiReferenceList implementsList = aClass.getImplementsList();
if (implementsList != null) {
final PsiJavaCodeReferenceElement[] referenceElements = implementsList.getReferenceElements();
if (referenceElements.length == 1) {
PsiClass returnClass = (PsiClass)referenceElements[0].resolve();
if (returnClass != null && returnClass.isInterface()) {
return returnClass;
}
}
}
return null;
}
}
@@ -1,51 +0,0 @@
package com.intellij.codeInspection.inheritance.search;
import com.intellij.psi.PsiClass;
import org.jetbrains.annotations.NotNull;
class InheritorsCountData implements Comparable<InheritorsCountData> {
@NotNull
private final PsiClass myPsiClass;
private final int myInheritorsCount;
public InheritorsCountData(@NotNull final PsiClass psiClass, final int inheritorsCount) {
myPsiClass = psiClass;
myInheritorsCount = inheritorsCount;
}
@Override
public boolean equals(final Object o) {
if (this == o) return true;
if (o == null || !(o instanceof InheritorsCountData)) return false;
final InheritorsCountData data = (InheritorsCountData)o;
return myInheritorsCount == data.myInheritorsCount && myPsiClass.equals(data.myPsiClass);
}
@NotNull
public PsiClass getPsiClass() {
return myPsiClass;
}
public int getInheritorsCount() {
return myInheritorsCount;
}
@Override
public int hashCode() {
final String name = myPsiClass.getName();
int result = name != null ? name.hashCode() : 0;
return 31 * result + myInheritorsCount;
}
@Override
public int compareTo(@NotNull final InheritorsCountData that) {
final int sub = -this.myInheritorsCount + that.myInheritorsCount;
if (sub != 0) return sub;
return String.CASE_INSENSITIVE_ORDER.compare(this.myPsiClass.getName(), that.myPsiClass.getName());
}
public String toString() {
return String.format("%s:%d", myPsiClass, myInheritorsCount);
}
}
@@ -1,148 +0,0 @@
/*
* Copyright 2000-2016 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.intellij.codeInspection.inheritance.search;
import com.intellij.openapi.util.Couple;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.psi.PsiClass;
import com.intellij.psi.search.GlobalSearchScope;
import com.intellij.psi.search.searches.DirectClassInheritorsSearch;
import com.intellij.util.Processor;
import org.jetbrains.annotations.NotNull;
import java.util.*;
public class InheritorsStatisticalDataSearch {
/**
* search for most used inheritors of superClass in scope
*
* @param aClass - class that excluded from inheritors of superClass
* @param minPercentRatio - head volume
* @return - search results in relevant ordering (frequency descent)
*/
public static List<InheritorsStatisticsSearchResult> search(final @NotNull PsiClass superClass,
final @NotNull PsiClass aClass,
final @NotNull GlobalSearchScope scope,
final int minPercentRatio) {
final String superClassName = superClass.getName();
final String aClassName = aClass.getName();
final Set<String> disabledNames = new HashSet<>();
disabledNames.add(aClassName);
disabledNames.add(superClassName);
final Set<InheritorsCountData> collector = new TreeSet<>();
final Couple<Integer> collectingResult = collectInheritorsInfo(superClass, collector, disabledNames);
final int allAnonymousInheritors = collectingResult.getSecond();
final int allInheritors = collectingResult.getFirst() + allAnonymousInheritors - 1;
final List<InheritorsStatisticsSearchResult> result = new ArrayList<>();
Integer firstPercent = null;
for (final InheritorsCountData data : collector) {
final int inheritorsCount = data.getInheritorsCount();
if (inheritorsCount < allAnonymousInheritors) {
break;
}
final int percent = (inheritorsCount * 100) / allInheritors;
if (percent < 1) {
break;
}
if (firstPercent == null) {
firstPercent = percent;
}
else if (percent * minPercentRatio < firstPercent) {
break;
}
final PsiClass psiClass = data.getPsiClass();
final VirtualFile file = psiClass.getContainingFile().getVirtualFile();
if (file != null && scope.contains(file)) {
result.add(new InheritorsStatisticsSearchResult(psiClass, percent));
}
}
return result;
}
private static Couple<Integer> collectInheritorsInfo(final PsiClass superClass,
final Set<InheritorsCountData> collector,
final Set<String> disabledNames) {
return collectInheritorsInfo(superClass, collector, disabledNames, new HashSet<>(), new HashSet<>());
}
private static Couple<Integer> collectInheritorsInfo(final PsiClass aClass,
final Set<InheritorsCountData> collector,
final Set<String> disabledNames,
final Set<String> processedElements,
final Set<String> allNotAnonymousInheritors) {
final String className = aClass.getName();
if (!processedElements.add(className)) return Couple.of(0, 0);
final MyInheritorsInfoProcessor processor = new MyInheritorsInfoProcessor(collector, disabledNames, processedElements);
DirectClassInheritorsSearch.search(aClass).forEach(processor);
allNotAnonymousInheritors.addAll(processor.getAllNotAnonymousInheritors());
final int allInheritorsCount = processor.getAllNotAnonymousInheritors().size() + processor.getAnonymousInheritorsCount();
if (!aClass.isInterface() && allInheritorsCount != 0 && !disabledNames.contains(className)) {
collector.add(new InheritorsCountData(aClass, allInheritorsCount));
}
return Couple.of(allNotAnonymousInheritors.size(), processor.getAnonymousInheritorsCount());
}
private static class MyInheritorsInfoProcessor implements Processor<PsiClass> {
private final Set<InheritorsCountData> myCollector;
private final Set<String> myDisabledNames;
private final Set<String> myProcessedElements;
private final Set<String> myAllNotAnonymousInheritors;
private MyInheritorsInfoProcessor(Set<InheritorsCountData> collector, Set<String> disabledNames, Set<String> processedElements) {
myCollector = collector;
myDisabledNames = disabledNames;
myProcessedElements = processedElements;
myAllNotAnonymousInheritors = new HashSet<>();
}
private int myAnonymousInheritorsCount;
private Set<String> getAllNotAnonymousInheritors() {
return myAllNotAnonymousInheritors;
}
private int getAnonymousInheritorsCount() {
return myAnonymousInheritorsCount;
}
@Override
public boolean process(final PsiClass psiClass) {
final String inheritorName = psiClass.getName();
if (inheritorName == null) {
myAnonymousInheritorsCount++;
}
else {
final Couple<Integer> res = collectInheritorsInfo(psiClass,
myCollector,
myDisabledNames,
myProcessedElements,
myAllNotAnonymousInheritors);
myAnonymousInheritorsCount += res.getSecond();
if (!psiClass.isInterface()) {
myAllNotAnonymousInheritors.add(inheritorName);
}
}
return true;
}
}
}
@@ -1,25 +0,0 @@
package com.intellij.codeInspection.inheritance.search;
import com.intellij.psi.PsiClass;
import org.jetbrains.annotations.NotNull;
public class InheritorsStatisticsSearchResult {
@NotNull
private final PsiClass myClass;
private final int myPercent;
InheritorsStatisticsSearchResult(final @NotNull PsiClass aClass, final int percent) {
myClass = aClass;
myPercent = percent;
}
public PsiClass getPsiClass() {
return myClass;
}
public int getPercent() {
return myPercent;
}
}
@@ -1,6 +1,6 @@
class Some {
void m() {
A someA = new A () {}<caret>
A someA = new A<caret> () {};
}
}
@@ -9,9 +9,9 @@ class A {}
class B extends A {}
class B1 extends B {}
class B2 extends B {}
class B3 extends B {}
class B4 extends B {}
class B5 extends B {}
class B3 extends B {}
class B6 extends B {}
@@ -1,22 +0,0 @@
class MyInheritor implement<caret>s A {
}
interface A {
}
interface B extends A {}
interface B1 extends A {}
interface B6 extends A {}
interface B2 extends A {}
interface B3 extends A {}
interface B4 extends A {}
interface B5 extends A {}
interface C extends B {}
interface C1 extends B {}
interface C2 extends B {}
interface C3 extends B {}
interface C4 extends B {}
class D extends C {}
@@ -1,25 +0,0 @@
class MyInheritor implement<caret>s A {}
interface A {}
interface B extends A {}
interface B1 extends A {}
interface B6 extends A {}
interface B2 extends A {}
interface B3 extends A {}
interface B4 extends A {}
interface B5 extends A {}
interface C extends B {}
interface C1 extends B {}
interface C2 extends B {}
interface C3 extends B {}
interface C4 extends B {}
class D implements C {}
class E1 extends D {}
class E2 extends D {}
class E3 extends D {}
class E4 extends D {}
class E5 extends D {}
@@ -1,4 +1,4 @@
class MyInheritor extends A {
class MyInheritor<caret> extends A {
}
@@ -18,20 +18,4 @@ class C32 extends B3 {}
class B4 extends A {}
class C41 extends B4 {}
class C42 extends B4 {}
class B5 extends A {}
class C51 extends B5 {}
class C52 extends B5 {}
class B6 extends A {}
class C61 extends B6 {}
class C62 extends B6 {}
class B7 extends A {}
class C71 extends B7 {}
class C72 extends B7 {}
class B8 extends A {}
class C81 extends B8 {}
class C82 extends B8 {}
class C42 extends B4 {}
@@ -4,10 +4,11 @@ import com.intellij.JavaTestUtil;
import com.intellij.codeInsight.intention.IntentionAction;
import com.intellij.codeInsight.intention.IntentionActionDelegate;
import com.intellij.codeInspection.ex.QuickFixWrapper;
import com.intellij.codeInspection.inheritance.ChangeSuperClassFix;
import com.intellij.codeInspection.inheritance.SuperClassHasFrequentlyUsedInheritorsInspection;
import com.intellij.compiler.CompilerReferencesTestBase;
import com.intellij.compiler.inspection.ChangeSuperClassFix;
import com.intellij.compiler.inspection.FrequentlyUsedInheritorInspection;
import com.intellij.openapi.util.Pair;
import com.intellij.testFramework.fixtures.JavaCodeInsightFixtureTestCase;
import com.intellij.testFramework.SkipSlowTestLocally;
import com.intellij.util.containers.ContainerUtil;
import com.intellij.util.containers.HashSet;
import org.jetbrains.annotations.Nullable;
@@ -15,37 +16,35 @@ import org.jetbrains.annotations.Nullable;
import java.util.List;
import java.util.Set;
public class SuperClassHasFrequentlyUsedInheritorsInspectionTest extends JavaCodeInsightFixtureTestCase {
@SkipSlowTestLocally
public class FrequentlyUsedInheritorInspectionTest extends CompilerReferencesTestBase {
@Override
public void setUp() throws Exception {
super.setUp();
installCompiler();
myFixture.enableInspections(FrequentlyUsedInheritorInspection.class);
}
@Override
protected String getTestDataPath() {
return JavaTestUtil.getJavaTestDataPath() + "/inspection/smartInheritance/";
}
//search tests
public void testRelevantClassShowed() {
doTest(Pair.create("C", 75), Pair.create("B", 91));
}
public void testInterfacesNotShowed() {
assertEmptyResult();
}
public void testInterfacesNotShowed2() {
doTest(Pair.create("D", 83));
doTest(Pair.create("B", 12));
}
public void testAnonymousClasses() {
doTest(Pair.create("B", 83));
doTest(Pair.create("B", 7));
}
public void testAnonymousClassesInStats() {
doTest(Pair.create("A", 62));
doTest(Pair.create("A", 6));
}
public void testAbstractClass() {
doTest(Pair.create("B", 85));
doTest(Pair.create("B", 7));
}
public void testNoCompletionForAbstractClasses() {
@@ -53,31 +52,26 @@ public class SuperClassHasFrequentlyUsedInheritorsInspectionTest extends JavaCod
}
public void testNoMoreThanMaxCountIntentions() {
doTest(SuperClassHasFrequentlyUsedInheritorsInspection.MAX_QUICK_FIX_COUNTS);
doTest(FrequentlyUsedInheritorInspection.MAX_RESULT);
}
// completion tests
private void assertEmptyResult() {
doTest();
}
private void doTest(final Pair<String, Integer>... expectedResults) {
myFixture.configureByFile(getTestName(false) + ".java");
myFixture.enableInspections(SuperClassHasFrequentlyUsedInheritorsInspection.class);
rebuildProject();
final Set<Pair<String, Integer>> actualSet = new HashSet<Pair<String, Integer>>();
for (Pair<String, Integer> pair : expectedResults) {
IntentionAction action = myFixture.findSingleIntention("Make extends '" + pair.getFirst() +
"' - " + pair.getSecond() +
"%");
IntentionAction action = myFixture.findSingleIntention("Make extends '" + pair.getFirst());
IntentionAction intentionAction = ((IntentionActionDelegate)action).getDelegate();
if (intentionAction instanceof QuickFixWrapper) {
ChangeSuperClassFix changeSuperClassFix = getQuickFixFromWrapper((QuickFixWrapper)intentionAction);
if (changeSuperClassFix != null) {
actualSet.add(Pair.create(changeSuperClassFix.getNewSuperClass().getQualifiedName(), changeSuperClassFix.getPercent()));
actualSet.add(Pair.create(changeSuperClassFix.getNewSuperClass().getQualifiedName(), changeSuperClassFix.getInheritorCount()));
}
}
}
@@ -87,7 +81,7 @@ public class SuperClassHasFrequentlyUsedInheritorsInspectionTest extends JavaCod
private void doTest(final int expectedSize) {
myFixture.configureByFile(getTestName(false) + ".java");
myFixture.enableInspections(SuperClassHasFrequentlyUsedInheritorsInspection.class);
rebuildProject();
List<IntentionAction> actions = myFixture.filterAvailableIntentions("Make extends '");
@@ -41,6 +41,8 @@ public interface LightRef extends RW.Savable {
}
interface LightClassHierarchyElementDef extends NamedLightRef {
LightClassHierarchyElementDef[] EMPTY_ARRAY = new LightClassHierarchyElementDef[0];
}
interface LightAnonymousClassDef extends LightClassHierarchyElementDef {
@@ -49,7 +49,7 @@ public class ConvertInterfaceToClassIntention extends Intention {
return false;
}
private static void changeInterfaceToClass(PsiClass anInterface) throws IncorrectOperationException {
public static void changeInterfaceToClass(PsiClass anInterface) throws IncorrectOperationException {
final PsiIdentifier nameIdentifier = anInterface.getNameIdentifier();
assert nameIdentifier != null;
final PsiElement whiteSpace = nameIdentifier.getPrevSibling();
@@ -1,6 +1,7 @@
<html>
<body>
This inspection finds commonly used base class that could be extended instead of implementing interface or extending abstract class.
For example: <code>java.util.List</code> could be implemented by inheriting from <code>java.util.AbstractList</code>.
<!-- tooltip end -->
The inspection works only if a project is built using IntelliJ IDEA build system and super class is located inside source files.
</body>
</html>
+4 -4
View File
@@ -815,11 +815,11 @@
<localInspection groupPath="Java" language="JAVA" shortName="MagicConstant" displayName="Magic Constant"
groupKey="group.names.probable.bugs" groupBundle="messages.InspectionsBundle" enabledByDefault="true" level="WARNING"
implementationClass="com.intellij.codeInspection.magicConstant.MagicConstantInspection" />
<localInspection groupPath="Java" language="JAVA" shortName="SuperClassHasFrequentlyUsedInheritors"
<localInspection groupPath="Java" language="JAVA" shortName="FrequentlyUsedInheritorInspection"
groupBundle="messages.InspectionsBundle"
groupKey="group.names.inheritance.issues" enabledByDefault="false" level="WARNING"
implementationClass="com.intellij.codeInspection.inheritance.SuperClassHasFrequentlyUsedInheritorsInspection"
displayName="Class may extend a commonly used base class instead of implementing interface"/>
groupKey="group.names.inheritance.issues" enabledByDefault="false" level="INFORMATION"
implementationClass="com.intellij.compiler.inspection.FrequentlyUsedInheritorInspection"
displayName="Class may extend a commonly used base class"/>
<localInspection language="UAST" shortName="ImplicitSubclassInspection"
bundle="messages.InspectionsBundle" key="inspection.implicit.subclass.display.name"
groupBundle="messages.InspectionsBundle" groupKey="group.names.inheritance.issues"