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();
}
@@ -0,0 +1,123 @@
/*
* 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.codeInsight.daemon.GroupNames;
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, HighPriorityAction {
@NotNull
private final SmartPsiElementPointer<PsiClass> myNewSuperClass;
@NotNull
private final SmartPsiElementPointer<PsiClass> myOldSuperClass;
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);
myInheritorCount = percent;
}
@NotNull
@TestOnly
public PsiClass getNewSuperClass() {
return ObjectUtils.notNull(myNewSuperClass.getElement());
}
@TestOnly
public int getInheritorCount() {
return myInheritorCount;
}
@NotNull
@Override
public String getName() {
return String.format("Make " + (myNewSuperIsInterface ? "implements" : "extends") + " '%s'", myNewSuperName);
}
@NotNull
@Override
public String getFamilyName() {
return GroupNames.INHERITANCE_GROUP_NAME;
}
@Override
public void applyFix(@NotNull final Project project, @NotNull final ProblemDescriptor problemDescriptor) {
final PsiClass oldSuperClass = myOldSuperClass.getElement();
final PsiClass newSuperClass = myNewSuperClass.getElement();
if (oldSuperClass == null || newSuperClass == null) return;
changeSuperClass((PsiClass)problemDescriptor.getPsiElement(), oldSuperClass, newSuperClass);
}
/**
* myOldSuperClass and myNewSuperClass can be interfaces or classes in any combination
* <p/>
* 1. not checks that myOldSuperClass is really super of aClass
* 2. not checks that myNewSuperClass not exists in currently existed supers
*/
private static void changeSuperClass(@NotNull final PsiClass aClass,
@NotNull final PsiClass oldSuperClass,
@NotNull final PsiClass newSuperClass) {
JavaPsiFacade psiFacade = JavaPsiFacade.getInstance(aClass.getProject());
PsiElementFactory factory = psiFacade.getElementFactory();
if (aClass instanceof PsiAnonymousClass) {
((PsiAnonymousClass)aClass).getBaseClassReference().replace(factory.createClassReferenceElement(newSuperClass));
return;
}
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();
}
}
PsiReferenceList list;
if (newSuperClass.isInterface()) {
list = aClass.getImplementsList();
}
else {
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);
}
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;
}
}
}