| 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); |
| } |
| } |
| } |
| |