blob: d82d81a67ca6ef48bdf588c7765fef261968f60a [file] [log] [blame]
package org.jetbrains.android.inspections;
import com.android.resources.ResourceType;
import com.intellij.codeInsight.intention.AbstractIntentionAction;
import com.intellij.codeInspection.*;
import com.intellij.navigation.GotoRelatedItem;
import com.intellij.openapi.editor.Editor;
import com.intellij.openapi.module.Module;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.Computable;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.psi.*;
import com.intellij.psi.search.GlobalSearchScope;
import com.intellij.psi.search.searches.ReferencesSearch;
import com.intellij.psi.util.PsiTreeUtil;
import com.intellij.psi.xml.XmlAttributeValue;
import com.intellij.psi.xml.XmlFile;
import com.intellij.util.IncorrectOperationException;
import com.intellij.util.Processor;
import com.intellij.util.containers.HashSet;
import com.intellij.util.xml.DomFileDescription;
import com.intellij.util.xml.DomManager;
import org.jetbrains.android.AndroidGotoRelatedProvider;
import org.jetbrains.android.dom.AndroidCreateOnClickHandlerAction;
import org.jetbrains.android.dom.converters.OnClickConverter;
import org.jetbrains.android.dom.layout.LayoutDomFileDescription;
import org.jetbrains.android.dom.menu.MenuDomFileDescription;
import org.jetbrains.android.facet.AndroidFacet;
import org.jetbrains.android.util.AndroidBundle;
import org.jetbrains.android.util.AndroidCommonUtils;
import org.jetbrains.android.util.AndroidResourceUtil;
import org.jetbrains.android.util.AndroidUtils;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.*;
/**
* @author Eugene.Kudelevsky
*/
public class AndroidMissingOnClickHandlerInspection extends LocalInspectionTool {
@NotNull
private static Collection<PsiClass> findRelatedActivities(@NotNull XmlFile file,
@NotNull AndroidFacet facet,
@NotNull DomFileDescription<?> description) {
if (description instanceof LayoutDomFileDescription) {
final Computable<List<GotoRelatedItem>> computable = AndroidGotoRelatedProvider.getLazyItemsForXmlFile(file, facet);
if (computable == null) {
return Collections.emptyList();
}
final List<GotoRelatedItem> items = computable.compute();
if (items.isEmpty()) {
return Collections.emptyList();
}
final PsiClass activityClass = findActivityClass(facet.getModule());
if (activityClass == null) {
return Collections.emptyList();
}
final List<PsiClass> result = new ArrayList<PsiClass>();
for (GotoRelatedItem item : items) {
final PsiElement element = item.getElement();
if (element instanceof PsiClass) {
final PsiClass aClass = (PsiClass)element;
if (aClass.isInheritor(activityClass, true)) {
result.add(aClass);
}
}
}
return result;
}
else {
return findRelatedActivitiesForMenu(file, facet);
}
}
@NotNull
private static Set<PsiClass> findRelatedActivitiesForMenu(@NotNull XmlFile file, @NotNull AndroidFacet facet) {
final String resType = ResourceType.MENU.getName();
final String resourceName = AndroidCommonUtils.getResourceName(resType, file.getName());
final PsiField[] fields = AndroidResourceUtil.findResourceFields(facet, resType, resourceName, true);
if (fields.length == 0) {
return Collections.emptySet();
}
final Module module = facet.getModule();
final GlobalSearchScope scope = module.getModuleScope(false);
final PsiClass activityClass = findActivityClass(module);
if (activityClass == null) {
return Collections.emptySet();
}
final Set<PsiClass> result = new HashSet<PsiClass>();
ReferencesSearch.search(fields[0], scope).forEach(new Processor<PsiReference>() {
@Override
public boolean process(PsiReference reference) {
final PsiElement element = reference.getElement();
if (element == null) {
return true;
}
final PsiClass aClass = PsiTreeUtil.getParentOfType(element, PsiClass.class);
if (aClass != null && !result.contains(aClass) && aClass.isInheritor(activityClass, true)) {
result.add(aClass);
}
return true;
}
});
return result;
}
@Nullable
public static PsiClass findActivityClass(@NotNull Module module) {
return JavaPsiFacade.getInstance(module.getProject())
.findClass(AndroidUtils.ACTIVITY_BASE_CLASS_NAME, module.getModuleWithDependenciesAndLibrariesScope(false));
}
@Override
public ProblemDescriptor[] checkFile(@NotNull PsiFile file, @NotNull InspectionManager manager, boolean isOnTheFly) {
if (!(file instanceof XmlFile)) {
return ProblemDescriptor.EMPTY_ARRAY;
}
final AndroidFacet facet = AndroidFacet.getInstance(file);
if (facet == null) {
return ProblemDescriptor.EMPTY_ARRAY;
}
final DomFileDescription<?> description = DomManager.getDomManager(file.getProject()).getDomFileDescription((XmlFile)file);
if (!(description instanceof LayoutDomFileDescription) &&
!(description instanceof MenuDomFileDescription)) {
return ProblemDescriptor.EMPTY_ARRAY;
}
final Collection<PsiClass> activities = findRelatedActivities((XmlFile)file, facet, description);
final MyVisitor visitor = new MyVisitor(manager, isOnTheFly, activities);
file.accept(visitor);
return visitor.myResult.toArray(new ProblemDescriptor[visitor.myResult.size()]);
}
private static class MyVisitor extends XmlRecursiveElementVisitor {
private final InspectionManager myInspectionManager;
private final boolean myOnTheFly;
private final Collection<PsiClass> myRelatedActivities;
final List<ProblemDescriptor> myResult = new ArrayList<ProblemDescriptor>();
private MyVisitor(@NotNull InspectionManager inspectionManager, boolean onTheFly, @NotNull Collection<PsiClass> relatedActivities) {
myInspectionManager = inspectionManager;
myOnTheFly = onTheFly;
myRelatedActivities = relatedActivities;
}
@Override
public void visitXmlAttributeValue(XmlAttributeValue value) {
for (PsiReference reference : value.getReferences()) {
if (!(reference instanceof OnClickConverter.MyReference)) {
continue;
}
final OnClickConverter.MyReference ref = (OnClickConverter.MyReference)reference;
final String methodName = ref.getValue();
if (methodName.isEmpty()) {
continue;
}
final ResolveResult[] results = ref.multiResolve(false);
final Set<PsiClass> resolvedClasses = new HashSet<PsiClass>();
final Set<PsiClass> resolvedClassesWithMistake = new HashSet<PsiClass>();
for (ResolveResult result : results) {
if (result instanceof OnClickConverter.MyResolveResult) {
final PsiElement element = result.getElement();
if (element != null) {
final PsiClass aClass = PsiTreeUtil.getParentOfType(element, PsiClass.class);
if (aClass != null) {
resolvedClasses.add(aClass);
if (!((OnClickConverter.MyResolveResult)result).hasCorrectSignature()) {
resolvedClassesWithMistake.add(aClass);
}
}
}
}
}
PsiClass activity = null;
for (PsiClass relatedActivity : myRelatedActivities) {
if (!containsOrExtends(resolvedClasses, relatedActivity)) {
activity = relatedActivity;
break;
}
else if (activity == null && containsOrExtends(resolvedClassesWithMistake, relatedActivity)) {
activity = relatedActivity;
}
}
if (activity != null) {
reportMissingOnClickProblem(ref, activity, methodName, resolvedClassesWithMistake.contains(activity));
}
else if (results.length == 0) {
myResult.add(myInspectionManager.createProblemDescriptor(
value, reference.getRangeInElement(), ProblemsHolder.unresolvedReferenceMessage(reference),
ProblemHighlightType.GENERIC_ERROR_OR_WARNING, myOnTheFly));
}
else if (resolvedClassesWithMistake.size() > 0) {
reportMissingOnClickProblem(ref, resolvedClassesWithMistake.iterator().next(), methodName, true);
}
}
}
/**
* Returns true if the given associated activity class is either found in the given set of
* classes, or (less likely) extends any of the classes in that set
*/
private static boolean containsOrExtends(@NotNull Set<PsiClass> resolvedClasses, @NotNull PsiClass relatedActivity) {
if (resolvedClasses.contains(relatedActivity)) {
return true;
}
for (PsiClass resolvedClass : resolvedClasses) {
if (relatedActivity.isInheritor(resolvedClass, false)) {
return true;
}
}
return false;
}
private void reportMissingOnClickProblem(OnClickConverter.MyReference reference,
PsiClass activity,
String methodName,
boolean incorrectSignature) {
String activityName = activity.getName();
if (activityName == null) {
activityName = "";
}
final String message =
incorrectSignature
? AndroidBundle.message("android.inspections.on.click.missing.incorrect.signature", methodName, activityName)
: AndroidBundle.message("android.inspections.on.click.missing.problem", methodName, activityName);
final LocalQuickFix[] fixes =
StringUtil.isJavaIdentifier(methodName)
? new LocalQuickFix[]{new MyQuickFix(methodName, reference.getConverter(), activity)}
: LocalQuickFix.EMPTY_ARRAY;
myResult.add(myInspectionManager.createProblemDescriptor(
reference.getElement(), reference.getRangeInElement(), message,
ProblemHighlightType.GENERIC_ERROR_OR_WARNING, myOnTheFly, fixes));
}
}
public static class MyQuickFix extends AbstractIntentionAction implements LocalQuickFix {
private final String myMethodName;
private final OnClickConverter myConverter;
private final PsiClass myClass;
private MyQuickFix(@NotNull String methodName, @NotNull OnClickConverter converter, @NotNull PsiClass aClass) {
myMethodName = methodName;
myConverter = converter;
myClass = aClass;
}
@NotNull
@Override
public String getName() {
return "Create '" + myMethodName + "(" + myConverter.getShortParameterName() + ")' in '" + myClass.getName() + "'";
}
@NotNull
@Override
public String getText() {
return getName();
}
@NotNull
@Override
public String getFamilyName() {
return getName();
}
@Override
public void invoke(@NotNull Project project, Editor editor, PsiFile file) throws IncorrectOperationException {
final String paramType = myConverter.getDefaultMethodParameterType(myClass);
AndroidCreateOnClickHandlerAction.addHandlerMethodAndNavigate(project, myClass, myMethodName, paramType);
}
@Override
public void applyFix(@NotNull Project project, @NotNull ProblemDescriptor descriptor) {
// it is called from inspection view or "fix all problems" action (for example) instead of invoke()
doApplyFix(project);
}
public void doApplyFix(@NotNull Project project) {
final String paramType = myConverter.getDefaultMethodParameterType(myClass);
AndroidCreateOnClickHandlerAction.addHandlerMethod(project, myClass, myMethodName, paramType);
}
}
}