blob: aa772f3376101c5c785806d7823f925f1d99a63c [file] [log] [blame]
/*
* Copyright 2000-2009 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.
*/
/*
* Created by IntelliJ IDEA.
* User: max
* Date: Dec 21, 2001
* Time: 8:46:41 PM
* To change template for new class use
* Code Style | Class Templates options (Tools | IDE Options).
*/
package com.intellij.codeInspection.visibility;
import com.intellij.ToolExtensionPoints;
import com.intellij.analysis.AnalysisScope;
import com.intellij.codeInsight.FileModificationService;
import com.intellij.codeInsight.daemon.GroupNames;
import com.intellij.codeInsight.daemon.impl.IdentifierUtil;
import com.intellij.codeInspection.*;
import com.intellij.codeInspection.ex.EntryPointsManager;
import com.intellij.codeInspection.reference.*;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.extensions.ExtensionPoint;
import com.intellij.openapi.extensions.Extensions;
import com.intellij.openapi.project.Project;
import com.intellij.psi.*;
import com.intellij.psi.search.GlobalSearchScope;
import com.intellij.psi.search.PsiNonJavaFileReferenceProcessor;
import com.intellij.psi.search.PsiSearchHelper;
import com.intellij.psi.util.PsiTreeUtil;
import com.intellij.psi.util.PsiUtil;
import com.intellij.usageView.UsageViewTypeLocation;
import com.intellij.util.IncorrectOperationException;
import org.jetbrains.annotations.NonNls;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import javax.swing.*;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
import java.awt.*;
import java.util.List;
public class VisibilityInspection extends GlobalJavaBatchInspectionTool {
private static final Logger LOG = Logger.getInstance("#com.intellij.codeInspection.visibility.VisibilityInspection");
public boolean SUGGEST_PACKAGE_LOCAL_FOR_MEMBERS = true;
public boolean SUGGEST_PACKAGE_LOCAL_FOR_TOP_CLASSES = true;
public boolean SUGGEST_PRIVATE_FOR_INNERS = false;
private static final String DISPLAY_NAME = InspectionsBundle.message("inspection.visibility.display.name");
@NonNls private static final String SHORT_NAME = "WeakerAccess";
private static final String CAN_BE_PRIVATE = InspectionsBundle.message("inspection.visibility.compose.suggestion", "private");
private static final String CAN_BE_PACKAGE_LOCAL = InspectionsBundle.message("inspection.visibility.compose.suggestion", "package local");
private static final String CAN_BE_PROTECTED = InspectionsBundle.message("inspection.visibility.compose.suggestion", "protected");
private class OptionsPanel extends JPanel {
private final JCheckBox myPackageLocalForMembersCheckbox;
private final JCheckBox myPrivateForInnersCheckbox;
private final JCheckBox myPackageLocalForTopClassesCheckbox;
private OptionsPanel() {
super(new GridBagLayout());
GridBagConstraints gc = new GridBagConstraints();
gc.fill = GridBagConstraints.HORIZONTAL;
gc.weightx = 1;
gc.weighty = 0;
gc.anchor = GridBagConstraints.NORTHWEST;
myPackageLocalForMembersCheckbox = new JCheckBox(InspectionsBundle.message("inspection.visibility.option"));
myPackageLocalForMembersCheckbox.setSelected(SUGGEST_PACKAGE_LOCAL_FOR_MEMBERS);
myPackageLocalForMembersCheckbox.getModel().addChangeListener(new ChangeListener() {
@Override
public void stateChanged(ChangeEvent e) {
SUGGEST_PACKAGE_LOCAL_FOR_MEMBERS = myPackageLocalForMembersCheckbox.isSelected();
}
});
gc.gridy = 0;
add(myPackageLocalForMembersCheckbox, gc);
myPackageLocalForTopClassesCheckbox = new JCheckBox(InspectionsBundle.message("inspection.visibility.option1"));
myPackageLocalForTopClassesCheckbox.setSelected(SUGGEST_PACKAGE_LOCAL_FOR_TOP_CLASSES);
myPackageLocalForTopClassesCheckbox.getModel().addChangeListener(new ChangeListener() {
@Override
public void stateChanged(ChangeEvent e) {
SUGGEST_PACKAGE_LOCAL_FOR_TOP_CLASSES = myPackageLocalForTopClassesCheckbox.isSelected();
}
});
gc.gridy = 1;
add(myPackageLocalForTopClassesCheckbox, gc);
myPrivateForInnersCheckbox = new JCheckBox(InspectionsBundle.message("inspection.visibility.option2"));
myPrivateForInnersCheckbox.setSelected(SUGGEST_PRIVATE_FOR_INNERS);
myPrivateForInnersCheckbox.getModel().addChangeListener(new ChangeListener() {
@Override
public void stateChanged(ChangeEvent e) {
SUGGEST_PRIVATE_FOR_INNERS = myPrivateForInnersCheckbox.isSelected();
}
});
gc.gridy = 2;
gc.weighty = 1;
add(myPrivateForInnersCheckbox, gc);
}
}
@Override
public JComponent createOptionsPanel() {
return new OptionsPanel();
}
@Override
@NotNull
public String getDisplayName() {
return DISPLAY_NAME;
}
@Override
@NotNull
public String getGroupDisplayName() {
return GroupNames.DECLARATION_REDUNDANCY;
}
@Override
@NotNull
public String getShortName() {
return SHORT_NAME;
}
@Override
@Nullable
public CommonProblemDescriptor[] checkElement(@NotNull final RefEntity refEntity,
@NotNull final AnalysisScope scope,
@NotNull final InspectionManager manager,
@NotNull final GlobalInspectionContext globalContext,
@NotNull final ProblemDescriptionsProcessor processor) {
if (refEntity instanceof RefJavaElement) {
final RefJavaElement refElement = (RefJavaElement)refEntity;
if (refElement instanceof RefParameter) return null;
if (refElement.isSyntheticJSP()) return null;
//ignore entry points.
if (refElement.isEntry()) return null;
//ignore implicit constructors. User should not be able to see them.
if (refElement instanceof RefImplicitConstructor) return null;
if (refElement instanceof RefField) {
final Boolean isEnumConstant = refElement.getUserData(RefField.ENUM_CONSTANT);
if (isEnumConstant != null && isEnumConstant.booleanValue()) return null;
}
//ignore library override methods.
if (refElement instanceof RefMethod) {
RefMethod refMethod = (RefMethod) refElement;
if (refMethod.isExternalOverride()) return null;
if (refMethod.isEntry()) return null;
}
//ignore anonymous classes. They do not have access modifiers.
if (refElement instanceof RefClass) {
RefClass refClass = (RefClass) refElement;
if (refClass.isAnonymous() || refClass.isEntry() || refClass.isTestCase() || refClass.isServlet() || refClass.isApplet() || refClass.isLocalClass()) return null;
if (isTopLevelClass(refClass) && !SUGGEST_PACKAGE_LOCAL_FOR_TOP_CLASSES) return null;
if (refClass.isAndroidPublic()) {
return null;
}
}
//ignore unreferenced code. They could be a potential entry points.
if (refElement.getInReferences().isEmpty()) return null;
//ignore interface members. They always have public access modifier.
if (refElement.getOwner() instanceof RefClass) {
RefClass refClass = (RefClass) refElement.getOwner();
if (refClass.isInterface()) return null;
}
String access = getPossibleAccess(refElement);
if (access != refElement.getAccessModifier() && access != null) {
final PsiElement element = refElement.getElement();
final PsiElement nameIdentifier = element != null ? IdentifierUtil.getNameIdentifier(element) : null;
if (nameIdentifier != null) {
final String message;
String quickFixName = "Make " + ElementDescriptionUtil.getElementDescription(element, UsageViewTypeLocation.INSTANCE) + " ";
if (access.equals(PsiModifier.PRIVATE)) {
message = CAN_BE_PRIVATE;
quickFixName += PsiModifier.PRIVATE;
}
else {
if (access.equals(PsiModifier.PACKAGE_LOCAL)) {
message = CAN_BE_PACKAGE_LOCAL;
quickFixName += "package local";
}
else {
message = CAN_BE_PROTECTED;
quickFixName += PsiModifier.PROTECTED;
}
}
return new ProblemDescriptor[]{manager.createProblemDescriptor(nameIdentifier,
message,
new AcceptSuggestedAccess(globalContext.getRefManager(), access, quickFixName),
ProblemHighlightType.GENERIC_ERROR_OR_WARNING, false)};
}
}
}
return null;
}
@Nullable
@PsiModifier.ModifierConstant
public String getPossibleAccess(@Nullable RefJavaElement refElement) {
if (refElement == null) return null;
String curAccess = refElement.getAccessModifier();
String weakestAccess = PsiModifier.PRIVATE;
if (isTopLevelClass(refElement) || isCalledOnSubClasses(refElement)) {
weakestAccess = PsiModifier.PACKAGE_LOCAL;
}
if (isAbstractMethod(refElement)) {
weakestAccess = PsiModifier.PROTECTED;
}
if (curAccess == weakestAccess) return curAccess;
while (true) {
String weakerAccess = getWeakerAccess(curAccess, refElement);
if (weakerAccess == null || RefJavaUtil.getInstance().compareAccess(weakerAccess, weakestAccess) < 0) break;
if (isAccessible(refElement, weakerAccess)) {
curAccess = weakerAccess;
}
else {
break;
}
}
return curAccess;
}
private static boolean isCalledOnSubClasses(RefElement refElement) {
return refElement instanceof RefMethod && ((RefMethod)refElement).isCalledOnSubClass();
}
private static boolean isAbstractMethod(RefElement refElement) {
return refElement instanceof RefMethod && ((RefMethod) refElement).isAbstract();
}
private static boolean isTopLevelClass(RefElement refElement) {
return refElement instanceof RefClass && RefJavaUtil.getInstance().getTopLevelClass(refElement) == refElement;
}
@Nullable
@PsiModifier.ModifierConstant
private String getWeakerAccess(@PsiModifier.ModifierConstant String curAccess, RefElement refElement) {
if (curAccess == PsiModifier.PUBLIC) {
return isTopLevelClass(refElement) ? PsiModifier.PACKAGE_LOCAL : PsiModifier.PROTECTED;
}
if (curAccess == PsiModifier.PROTECTED) {
return SUGGEST_PACKAGE_LOCAL_FOR_MEMBERS ? PsiModifier.PACKAGE_LOCAL : PsiModifier.PRIVATE;
}
if (curAccess == PsiModifier.PACKAGE_LOCAL) {
return PsiModifier.PRIVATE;
}
return null;
}
private boolean isAccessible(RefJavaElement to, @PsiModifier.ModifierConstant String accessModifier) {
for (RefElement refElement : to.getInReferences()) {
if (!isAccessibleFrom(refElement, to, accessModifier)) return false;
}
if (to instanceof RefMethod) {
RefMethod refMethod = (RefMethod) to;
if (refMethod.isAbstract() && (refMethod.getDerivedMethods().isEmpty() || refMethod.getAccessModifier() == PsiModifier.PRIVATE)) return false;
for (RefMethod refOverride : refMethod.getDerivedMethods()) {
if (!isAccessibleFrom(refOverride, to, accessModifier)) return false;
}
for (RefMethod refSuper : refMethod.getSuperMethods()) {
if (RefJavaUtil.getInstance().compareAccess(refSuper.getAccessModifier(), accessModifier) > 0) return false;
}
}
if (to instanceof RefClass) {
RefClass refClass = (RefClass) to;
for (RefClass subClass : refClass.getSubClasses()) {
if (!isAccessibleFrom(subClass, to, accessModifier)) return false;
}
List children = refClass.getChildren();
if (children != null) {
for (Object refElement : children) {
if (!isAccessible((RefJavaElement)refElement, accessModifier)) return false;
}
}
for (final RefElement refElement : refClass.getInTypeReferences()) {
if (!isAccessibleFrom(refElement, refClass, accessModifier)) return false;
}
List<RefJavaElement> classExporters = ((RefClassImpl)refClass).getClassExporters();
if (classExporters != null) {
for (RefJavaElement refExporter : classExporters) {
if (getAccessLevel(accessModifier) < getAccessLevel(refExporter.getAccessModifier())) return false;
}
}
}
return true;
}
private static int getAccessLevel(@PsiModifier.ModifierConstant String access) {
if (access == PsiModifier.PRIVATE) return 1;
if (access == PsiModifier.PACKAGE_LOCAL) return 2;
if (access == PsiModifier.PROTECTED) return 3;
return 4;
}
private boolean isAccessibleFrom(RefElement from, RefJavaElement to, String accessModifier) {
if (accessModifier == PsiModifier.PUBLIC) return true;
final RefJavaUtil refUtil = RefJavaUtil.getInstance();
if (accessModifier == PsiModifier.PACKAGE_LOCAL) {
return RefJavaUtil.getPackage(from) == RefJavaUtil.getPackage(to);
}
RefClass fromTopLevel = refUtil.getTopLevelClass(from);
RefClass toTopLevel = refUtil.getTopLevelClass(to);
RefClass fromOwner = refUtil.getOwnerClass(from);
RefClass toOwner = refUtil.getOwnerClass(to);
if (accessModifier == PsiModifier.PROTECTED) {
if (SUGGEST_PRIVATE_FOR_INNERS) {
return refUtil.isInheritor(fromTopLevel, toOwner)
|| fromOwner != null && refUtil.isInheritor(fromOwner, toTopLevel)
|| toOwner != null && refUtil.getOwnerClass(toOwner) == from;
}
return refUtil.isInheritor(fromTopLevel, toOwner);
}
if (accessModifier == PsiModifier.PRIVATE) {
if (SUGGEST_PRIVATE_FOR_INNERS) {
if (isInExtendsList(to, fromTopLevel.getElement().getExtendsList())) return false;
if (isInExtendsList(to, fromTopLevel.getElement().getImplementsList())) return false;
if (isInAnnotations(to, fromTopLevel)) return false;
return fromTopLevel == toOwner || fromOwner == toTopLevel || toOwner != null && refUtil.getOwnerClass(toOwner) == from;
}
if (fromOwner != null && fromOwner.isStatic() && !to.isStatic() && refUtil.isInheritor(fromOwner, toOwner)) return false;
if (fromTopLevel == toOwner) {
if (from instanceof RefClass && to instanceof RefClass) {
final PsiClass fromClass = ((RefClass)from).getElement();
LOG.assertTrue(fromClass != null);
if (isInExtendsList(to, fromClass.getExtendsList())) return false;
if (isInExtendsList(to, fromClass.getImplementsList())) return false;
}
return true;
}
}
return false;
}
private static boolean isInAnnotations(final RefJavaElement to, final RefClass fromTopLevel) {
final PsiModifierList modifierList = fromTopLevel.getElement().getModifierList();
if (modifierList == null) return false;
final PsiElement toElement = to.getElement();
final boolean [] resolved = new boolean[] {false};
modifierList.accept(new JavaRecursiveElementWalkingVisitor() {
@Override
public void visitReferenceExpression(PsiReferenceExpression expression) {
if (resolved[0]) return;
super.visitReferenceExpression(expression);
if (expression.resolve() == toElement) {
resolved[0] = true;
}
}
});
return resolved[0];
}
private static boolean isInExtendsList(final RefJavaElement to, final PsiReferenceList extendsList) {
if (extendsList != null) {
final PsiJavaCodeReferenceElement[] referenceElements = extendsList.getReferenceElements();
for (PsiJavaCodeReferenceElement referenceElement : referenceElements) {
final PsiReferenceParameterList parameterList = referenceElement.getParameterList();
if (parameterList != null) {
for (PsiType type : parameterList.getTypeArguments()) {
if (extendsList.getManager().areElementsEquivalent(PsiUtil.resolveClassInType(type), to.getElement())) {
return true;
}
}
}
}
}
return false;
}
@Override
protected boolean queryExternalUsagesRequests(@NotNull final RefManager manager,
@NotNull final GlobalJavaInspectionContext globalContext,
@NotNull final ProblemDescriptionsProcessor processor) {
final EntryPointsManager entryPointsManager = globalContext.getEntryPointsManager(manager);
for (RefElement entryPoint : entryPointsManager.getEntryPoints()) {
ignoreElement(processor, entryPoint);
}
ExtensionPoint<VisibilityExtension> point = Extensions.getRootArea().getExtensionPoint(ToolExtensionPoints.VISIBLITY_TOOL);
for (VisibilityExtension addin : point.getExtensions()) {
addin.fillIgnoreList(manager, processor);
}
manager.iterate(new RefJavaVisitor() {
@Override public void visitElement(@NotNull final RefEntity refEntity) {
if (!(refEntity instanceof RefElement)) return;
if (processor.getDescriptions(refEntity) == null) return;
refEntity.accept(new RefJavaVisitor() {
@Override public void visitField(@NotNull final RefField refField) {
if (refField.getAccessModifier() != PsiModifier.PRIVATE) {
globalContext.enqueueFieldUsagesProcessor(refField, new GlobalJavaInspectionContext.UsagesProcessor() {
@Override
public boolean process(PsiReference psiReference) {
ignoreElement(processor, refField);
return false;
}
});
}
}
@Override public void visitMethod(@NotNull final RefMethod refMethod) {
if (!refMethod.isExternalOverride() && refMethod.getAccessModifier() != PsiModifier.PRIVATE &&
!(refMethod instanceof RefImplicitConstructor)) {
globalContext.enqueueDerivedMethodsProcessor(refMethod, new GlobalJavaInspectionContext.DerivedMethodsProcessor() {
@Override
public boolean process(PsiMethod derivedMethod) {
ignoreElement(processor, refMethod);
return false;
}
});
globalContext.enqueueMethodUsagesProcessor(refMethod, new GlobalJavaInspectionContext.UsagesProcessor() {
@Override
public boolean process(PsiReference psiReference) {
ignoreElement(processor, refMethod);
return false;
}
});
if (entryPointsManager.isAddNonJavaEntries()) {
final RefClass ownerClass = refMethod.getOwnerClass();
if (refMethod.isConstructor() && ownerClass.getDefaultConstructor() != null) {
String qualifiedName = ownerClass.getElement().getQualifiedName();
if (qualifiedName != null) {
final Project project = manager.getProject();
PsiSearchHelper.SERVICE.getInstance(project)
.processUsagesInNonJavaFiles(qualifiedName, new PsiNonJavaFileReferenceProcessor() {
@Override
public boolean process(PsiFile file, int startOffset, int endOffset) {
entryPointsManager.addEntryPoint(refMethod, false);
ignoreElement(processor, refMethod);
return false;
}
}, GlobalSearchScope.projectScope(project));
}
}
}
}
}
@Override public void visitClass(@NotNull final RefClass refClass) {
if (!refClass.isAnonymous()) {
globalContext.enqueueDerivedClassesProcessor(refClass, new GlobalJavaInspectionContext.DerivedClassesProcessor() {
@Override
public boolean process(PsiClass inheritor) {
ignoreElement(processor, refClass);
return false;
}
});
globalContext.enqueueClassUsagesProcessor(refClass, new GlobalJavaInspectionContext.UsagesProcessor() {
@Override
public boolean process(PsiReference psiReference) {
ignoreElement(processor, refClass);
return false;
}
});
}
}
});
}
});
return false;
}
private static void ignoreElement(@NotNull ProblemDescriptionsProcessor processor, @NotNull RefEntity refElement){
processor.ignoreElement(refElement);
if (refElement instanceof RefClass) {
RefClass refClass = (RefClass) refElement;
RefMethod defaultConstructor = refClass.getDefaultConstructor();
if (defaultConstructor != null) {
processor.ignoreElement(defaultConstructor);
return;
}
}
RefEntity owner = refElement.getOwner();
if (owner instanceof RefElement) {
processor.ignoreElement(owner);
}
}
@Override
public void compose(@NotNull final StringBuffer buf, @NotNull final RefEntity refEntity, @NotNull final HTMLComposer composer) {
composer.appendElementInReferences(buf, (RefElement)refEntity);
}
@Override
@Nullable
public QuickFix getQuickFix(final String hint) {
return new AcceptSuggestedAccess(null, hint, null);
}
@Override
@Nullable
public String getHint(@NotNull final QuickFix fix) {
return ((AcceptSuggestedAccess)fix).getHint();
}
private static class AcceptSuggestedAccess implements LocalQuickFix{
private final RefManager myManager;
@PsiModifier.ModifierConstant private final String myHint;
private String myName;
private AcceptSuggestedAccess(final RefManager manager, @PsiModifier.ModifierConstant String hint, String name) {
myManager = manager;
myHint = hint;
myName = name;
}
@Override
@NotNull
public String getName() {
return myName != null ? myName : getFamilyName();
}
@Override
@NotNull
public String getFamilyName() {
return InspectionsBundle.message("inspection.visibility.accept.quickfix");
}
@Override
public void applyFix(@NotNull Project project, @NotNull ProblemDescriptor descriptor) {
if (!FileModificationService.getInstance().preparePsiElementForWrite(descriptor.getPsiElement())) return;
final PsiModifierListOwner element = PsiTreeUtil.getParentOfType(descriptor.getPsiElement(), PsiModifierListOwner.class);
if (element != null) {
RefElement refElement = null;
if (myManager != null) {
refElement = myManager.getReference(element);
}
try {
if (element instanceof PsiVariable) {
((PsiVariable)element).normalizeDeclaration();
}
PsiModifierList list = element.getModifierList();
LOG.assertTrue(list != null);
if (element instanceof PsiMethod) {
PsiMethod psiMethod = (PsiMethod)element;
PsiClass containingClass = psiMethod.getContainingClass();
if (containingClass != null && containingClass.getParent() instanceof PsiFile &&
myHint == PsiModifier.PRIVATE &&
list.hasModifierProperty(PsiModifier.FINAL)) {
list.setModifierProperty(PsiModifier.FINAL, false);
}
}
list.setModifierProperty(myHint, true);
if (refElement instanceof RefJavaElement) {
RefJavaUtil.getInstance().setAccessModifier((RefJavaElement)refElement, myHint);
}
}
catch (IncorrectOperationException e) {
LOG.error(e);
}
}
}
public String getHint() {
return myHint;
}
}
}