/* * Copyright 2000-2013 JetBrains s.r.o. * Copyright 2014-2014 AS3Boyan * Copyright 2014-2014 Elias Ku * * 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.plugins.haxe.ide.hierarchy; import com.intellij.codeInsight.TargetElementUtil; import com.intellij.codeInsight.TargetElementUtilBase; import com.intellij.openapi.actionSystem.CommonDataKeys; import com.intellij.openapi.actionSystem.DataContext; import com.intellij.openapi.diagnostic.Logger; import com.intellij.openapi.editor.Editor; import com.intellij.openapi.project.Project; import com.intellij.plugins.haxe.lang.psi.*; import com.intellij.plugins.haxe.lang.psi.impl.AbstractHaxePsiClass; import com.intellij.plugins.haxe.lang.psi.impl.AnonymousHaxeTypeImpl; import com.intellij.psi.*; import com.intellij.psi.impl.source.resolve.reference.impl.PsiMultiReference; import com.intellij.psi.impl.source.tree.LeafPsiElement; import com.intellij.psi.util.MethodSignatureUtil; import org.apache.commons.lang.NotImplementedException; import org.apache.log4j.Level; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import java.util.ArrayList; /** * Created by ebishton on 9/4/14. * * A set of utility functions that support the HierarchyProviders. */ public class HaxeHierarchyUtils { private static final Logger LOG = Logger.getInstance("#com.intellij.ide.hierarchy.HaxeHierarchyUtils"); static { LOG.setLevel(Level.DEBUG); } private HaxeHierarchyUtils() { throw new NotImplementedException("Static use only."); } /** * Given a PSI id element, find out if it -- or one of its parents -- * references a class, and, if so, returns the PSI element for the class. * * @param id A PSI element for an identifier (e.g. variable name). * @return A PSI class element, or null if not found. */ @Nullable public static HaxeClass findReferencedClassForId(@NotNull LeafPsiElement id) { if (null == id) { return null; } PsiReference found = id.findReferenceAt(0); PsiElement resolved = null; if (found instanceof PsiMultiReference) { for (PsiReference ref : ((PsiMultiReference)found).getReferences()) { PsiElement target = ref.resolve(); if (null != target && target instanceof PsiClass) { resolved = target; break; } } } else { resolved = found.resolve(); } if (LOG.isDebugEnabled()) { LOG.debug("findReferencedClassForID found " + resolved); } return ((resolved instanceof HaxeClass) ? ((HaxeClass) resolved) : null); } /** * Retrieve the list of classes implemented in the given File. * * @param psiRoot - File to search. * @return An array of found classes, or an empty array if none. */ public static HaxeClass[] getClassList(@NotNull HaxeFile psiRoot) { ArrayList<HaxeClass> classes = new ArrayList<HaxeClass>(); for (PsiElement child : psiRoot.getChildren()) { if (child instanceof HaxeClass) { classes.add((HaxeClass)child); } } HaxeClass[] return_type = {}; return (classes.toArray(return_type)); } /** * Get the PSI element for the class containing the currently focused * element. Anonymous classes can be excluded if desired. * * @param context - editing context * @param allowAnonymous - flag to allow anonymous classes or not. * @return The PSI element representing the containing class. */ @Nullable public static AbstractHaxePsiClass getContainingClass(@NotNull DataContext context, boolean allowAnonymous) { if (LOG.isDebugEnabled()) { LOG.debug("getContainingClass " + context); } final Project project = CommonDataKeys.PROJECT.getData(context); if (project == null) { if (LOG.isDebugEnabled()) { LOG.debug("No project"); } return null; } final Editor editor = CommonDataKeys.EDITOR.getData(context); if (LOG.isDebugEnabled()) { LOG.debug("editor " + editor); } if (editor != null) { final PsiFile file = PsiDocumentManager.getInstance(project).getPsiFile(editor.getDocument()); if (file == null) { if (LOG.isDebugEnabled()) { LOG.debug("No file found."); } return null; } final PsiElement targetElement = TargetElementUtilBase.findTargetElement(editor, TargetElementUtilBase.ELEMENT_NAME_ACCEPTED | TargetElementUtilBase.REFERENCED_ELEMENT_ACCEPTED | TargetElementUtilBase.LOOKUP_ITEM_ACCEPTED); if (LOG.isDebugEnabled()) { LOG.debug("target element " + targetElement); } if (targetElement instanceof AbstractHaxePsiClass) { return (AbstractHaxePsiClass)targetElement; } // Haven't found it yet, walk the PSI tree toward the root. final int offset = editor.getCaretModel().getOffset(); PsiElement element = file.findElementAt(offset); while (element != null) { if (LOG.isDebugEnabled()) { LOG.debug("context element " + element); } if (element instanceof HaxeFile) { // If we get to the file node, then we're outside of a class definition. // No need to look further. return null; } if (element instanceof AbstractHaxePsiClass) { // Keep looking if we don't allow anonymous classes. if (allowAnonymous || !(element instanceof AnonymousHaxeTypeImpl)) { return (AbstractHaxePsiClass)element; } } element = element.getParent(); } return null; } else { final PsiElement element = CommonDataKeys.PSI_ELEMENT.getData(context); return element instanceof AbstractHaxePsiClass ? (AbstractHaxePsiClass)element : null; } } /** * Retrieve the PSI element for the file containing the given * context (focus element). * * @param context - editing context * @return The PSI node representing the file element. */ @Nullable public static HaxeFile getContainingFile(@NotNull DataContext context) { if (LOG.isDebugEnabled()) { LOG.debug("getContainingFile " + context); } // XXX: EMB: Can we just ask for the node at offset 0?? PsiElement element = getPsiElement(context); while (element != null) { if (element instanceof HaxeFile) { return (HaxeFile)element; } element = element.getParent(); } return null; } /** * Retrieve the PSI element for the given context (focal point). * Returns the leaf-node element at the exact position in the PSI. * This does NOT attempt to locate a higher-order PSI element as * {@link TargetElementUtilBase#findTargetElement} would. * * @param context - editing context * @return The PSI element at the caret position. */ @Nullable public static PsiElement getPsiElement(@NotNull DataContext context) { if (LOG.isDebugEnabled()) { LOG.debug("getPsiElement " + context); } PsiElement element = null; final Project project = CommonDataKeys.PROJECT.getData(context); if (project == null) { if (LOG.isDebugEnabled()) { LOG.debug("No project"); } return null; } final Editor editor = CommonDataKeys.EDITOR.getData(context); if (LOG.isDebugEnabled()) { LOG.debug("editor " + editor); } if (editor != null) { final PsiFile file = PsiDocumentManager.getInstance(project).getPsiFile(editor.getDocument()); if (file == null) { if (LOG.isDebugEnabled()) { LOG.debug("No file found."); } return null; } final int offset = editor.getCaretModel().getOffset(); element = file.findElementAt(offset); } else { element = CommonDataKeys.PSI_ELEMENT.getData(context); } return element; } @Nullable public static PsiElement getReferencedElement(@NotNull DataContext context) { PsiElement element = null; final Editor editor = CommonDataKeys.EDITOR.getData(context); if (editor != null) { element = TargetElementUtil.findTargetElement(editor, TargetElementUtil.REFERENCED_ELEMENT_ACCEPTED | TargetElementUtil.ELEMENT_NAME_ACCEPTED); } return element; } /** * Determine if there is a method that is the target of the current * action, and, if so, return it. * * @param context Editor context. * @return The PSI method if the current context points at a method, * null otherwise. */ @Nullable public static HaxeMethod getTargetMethod(@NotNull DataContext context) { if (LOG.isDebugEnabled()) { LOG.debug("getTargetMethod " + context); } final Project project = CommonDataKeys.PROJECT.getData(context); if (null == project) { if (LOG.isDebugEnabled()) { LOG.debug("No project"); } return null; } final Editor editor = CommonDataKeys.EDITOR.getData(context); if (null == editor) { if (LOG.isDebugEnabled()) { LOG.debug("No editor"); } final PsiElement element = CommonDataKeys.PSI_ELEMENT.getData(context); return element instanceof HaxeMethod ? (HaxeMethod) element : null; } if (LOG.isDebugEnabled()) { LOG.debug("editor " + editor); } final PsiFile file = PsiDocumentManager.getInstance(project).getPsiFile(editor.getDocument()); if (file == null) { if (LOG.isDebugEnabled()) { LOG.debug("No file found."); } return null; } final PsiElement targetElement = TargetElementUtilBase.findTargetElement(editor, TargetElementUtilBase.ELEMENT_NAME_ACCEPTED | TargetElementUtilBase.REFERENCED_ELEMENT_ACCEPTED | TargetElementUtilBase.LOOKUP_ITEM_ACCEPTED); if (LOG.isDebugEnabled()) { LOG.debug("target element " + targetElement); } if (targetElement instanceof HaxeMethod) { LOG.debug("target element " + targetElement); return ((HaxeMethod) targetElement); } // Haven't found it yet, walk the PSI tree toward the root. final int offset = editor.getCaretModel().getOffset(); PsiElement element = file.findElementAt(offset); while (element != null) { if (LOG.isDebugEnabled()) { LOG.debug("context element " + element); } if (element instanceof HaxeFile) { // If we get to the file node, then we're outside of a class definition. // No need to look further. return null; } if (element instanceof HaxeMethod) { return ((HaxeMethod)element); } element = element.getParent(); } return null; } /** * Determine the class (PSI element), if any, that is referenced by the * given reference expression. * * @param element A PSI reference expression. * @return The associated class, if any. null if not found. */ @Nullable public static HaxeClass resolveClassReference(@NotNull HaxeReference element) { HaxeClassResolveResult result = element.resolveHaxeClass(); HaxeClass pclass = result == null ? null : result.getHaxeClass(); return pclass; } // Lifted from MethodHierarchyUtil, which needed the cannotBeOverriding helper // to be overridden. /** * Locates a potentially overridden method in a sub-class or an * intermediate parent -- up to the class containing the base method. * * Note: This CANNOT be used to find a method in a super-class of * baseMethod's class. Only classes that are sub-classes of baseMethod are * considered. * * @param baseMethod The method that may be overridden * @param aClass The sub-class to start inspecting at. * @param checkBases Whether to continue to further base classes. * @return The PsiMethod in the given class or the closest superclass. */ public static PsiMethod findBaseMethodInClass(final PsiMethod baseMethod, final PsiClass aClass, final boolean checkBases) { if (baseMethod == null) return null; // base method is invalid if (cannotBeOverriding(baseMethod)) return null; return MethodSignatureUtil.findMethodBySuperMethod(aClass, baseMethod, checkBases); } /** * Figure out if a method can override a lower one. * @param method The method to test. * @return true if the method can override one in a superclass, false if not. */ public static boolean cannotBeOverriding(final PsiMethod method) { // Note that in Haxe, a private method can override another private // method, and so can a public method (but private can't override public). final PsiClass parentClass = method.getContainingClass(); return parentClass == null || method.isConstructor() || method.hasModifierProperty(PsiModifier.STATIC); } } // END class HaxeHierarchyUtils