package fr.adrienbrault.idea.symfony2plugin.util; import com.intellij.codeInsight.completion.CompletionResultSet; import com.intellij.openapi.project.Project; import com.intellij.patterns.ElementPattern; import com.intellij.patterns.PlatformPatterns; import com.intellij.patterns.PsiElementPattern; import com.intellij.psi.*; import com.intellij.psi.util.PsiTreeUtil; import com.intellij.util.Processor; import com.jetbrains.php.PhpIndex; import com.jetbrains.php.codeInsight.PhpCodeInsightUtil; import com.jetbrains.php.completion.PhpLookupElement; import com.jetbrains.php.lang.PhpLangUtil; import com.jetbrains.php.lang.PhpLanguage; import com.jetbrains.php.lang.lexer.PhpTokenTypes; import com.jetbrains.php.lang.parser.PhpElementTypes; import com.jetbrains.php.lang.patterns.PhpPatterns; import com.jetbrains.php.lang.psi.PhpFile; import com.jetbrains.php.lang.psi.PhpPsiElementFactory; import com.jetbrains.php.lang.psi.PhpPsiUtil; import com.jetbrains.php.lang.psi.elements.*; import com.jetbrains.php.lang.psi.resolve.types.PhpType; import com.jetbrains.php.phpunit.PhpUnitUtil; import com.jetbrains.php.refactoring.PhpAliasImporter; import fr.adrienbrault.idea.symfony2plugin.dic.MethodReferenceBag; import fr.adrienbrault.idea.symfony2plugin.util.psi.PsiElementAssertUtil; import org.apache.commons.lang.StringUtils; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import java.util.*; /** * @author Daniel Espendiller <daniel@espendiller.net> */ public class PhpElementsUtil { static public List<ResolveResult> getClassInterfaceResolveResult(Project project, String fqnClassOrInterfaceName) { // api workaround for at least interfaces if(!fqnClassOrInterfaceName.startsWith("\\")) { fqnClassOrInterfaceName = "\\" + fqnClassOrInterfaceName; } List<ResolveResult> results = new ArrayList<>(); for (PhpClass phpClass : PhpIndex.getInstance(project).getAnyByFQN(fqnClassOrInterfaceName)) { results.add(new PsiElementResolveResult(phpClass)); } return results; } /** * Gets all array keys as string of an ArrayCreationExpression * * ['foo' => $bar] */ @NotNull static public Collection<String> getArrayCreationKeys(@NotNull ArrayCreationExpression arrayCreationExpression) { return getArrayCreationKeyMap(arrayCreationExpression).keySet(); } /** * Gets array key-value as single PsiElement map * * ['foo' => $bar] */ @NotNull static public Map<String, PsiElement> getArrayCreationKeyMap(@NotNull ArrayCreationExpression arrayCreationExpression) { Map<String, PsiElement> keys = new HashMap<>(); for(ArrayHashElement arrayHashElement: arrayCreationExpression.getHashElements()) { PhpPsiElement child = arrayHashElement.getKey(); if(child instanceof StringLiteralExpression) { keys.put(((StringLiteralExpression) child).getContents(), child); } } return keys; } /** * Gets string values of array * * ["value", "value2"] */ @NotNull static public Set<String> getArrayValuesAsString(@NotNull ArrayCreationExpression arrayCreationExpression) { return getArrayValuesAsMap(arrayCreationExpression).keySet(); } /** * Get array string values mapped with their PsiElements * * ["value", "value2"] */ @NotNull static public Map<String, PsiElement> getArrayValuesAsMap(@NotNull ArrayCreationExpression arrayCreationExpression) { List<PsiElement> arrayValues = PhpPsiUtil.getChildren(arrayCreationExpression, psiElement -> psiElement.getNode().getElementType() == PhpElementTypes.ARRAY_VALUE ); if(arrayValues == null) { return Collections.emptyMap(); } Map<String, PsiElement> keys = new HashMap<>(); for (PsiElement child : arrayValues) { String stringValue = PhpElementsUtil.getStringValue(child.getFirstChild()); if(stringValue != null && StringUtils.isNotBlank(stringValue)) { keys.put(stringValue, child); } } return keys; } /** * array('foo' => 'bar', 'foo1' => 'bar', 1 => 'foo') */ @NotNull static public HashMap<String, String> getArrayKeyValueMap(@NotNull ArrayCreationExpression arrayCreationExpression) { HashMap<String, String> keys = new HashMap<>(); for(ArrayHashElement arrayHashElement: arrayCreationExpression.getHashElements()) { PhpPsiElement child = arrayHashElement.getKey(); if(child != null && ((child instanceof StringLiteralExpression) || PhpPatterns.psiElement(PhpElementTypes.NUMBER).accepts(child))) { String key; if(child instanceof StringLiteralExpression) { key = ((StringLiteralExpression) child).getContents(); } else { key = child.getText(); } if(key == null || StringUtils.isBlank(key)) { continue; } String value = null; if(arrayHashElement.getValue() instanceof StringLiteralExpression) { value = ((StringLiteralExpression) arrayHashElement.getValue()).getContents(); } if(value == null || StringUtils.isBlank(value)) { continue; } keys.put(key, value); } } return keys; } @Nullable static public PhpPsiElement getArrayValue(ArrayCreationExpression arrayCreationExpression, String name) { for(ArrayHashElement arrayHashElement: arrayCreationExpression.getHashElements()) { PhpPsiElement child = arrayHashElement.getKey(); if(child instanceof StringLiteralExpression) { if(((StringLiteralExpression) child).getContents().equals(name)) { return arrayHashElement.getValue(); } } } return null; } @Nullable static public String getArrayValueString(ArrayCreationExpression arrayCreationExpression, String name) { PhpPsiElement phpPsiElement = getArrayValue(arrayCreationExpression, name); if(phpPsiElement == null) { return null; } if(phpPsiElement instanceof StringLiteralExpression) { return ((StringLiteralExpression) phpPsiElement).getContents(); } return null; } static public PsiElement[] getPsiElementsBySignature(Project project, @Nullable String signature) { if(signature == null) { return new PsiElement[0]; } Collection<? extends PhpNamedElement> phpNamedElementCollections = PhpIndex.getInstance(project).getBySignature(signature, null, 0); return phpNamedElementCollections.toArray(new PsiElement[phpNamedElementCollections.size()]); } @Nullable static public PsiElement getPsiElementsBySignatureSingle(Project project, @Nullable String signature) { PsiElement[] psiElements = getPsiElementsBySignature(project, signature); if(psiElements.length == 0) { return null; } return psiElements[0]; } @Deprecated static public PsiElement[] getClassInterfacePsiElements(Project project, String FQNClassOrInterfaceName) { // convert ResolveResult to PsiElement List<PsiElement> results = new ArrayList<>(); for(ResolveResult result: getClassInterfaceResolveResult(project, FQNClassOrInterfaceName)) { results.add(result.getElement()); } return results.toArray(new PsiElement[results.size()]); } /** * There is no need for this proxy method. * We are api safe now */ @Deprecated @Nullable static public Method getClassMethod(PhpClass phpClass, String methodName) { return phpClass.findMethodByName(methodName); } @Nullable static public Method getClassMethod(@NotNull Project project, @NotNull String phpClassName, @NotNull String methodName) { // we need here an each; because eg Command is non unique because phar file for(PhpClass phpClass: PhpIndex.getInstance(project).getClassesByFQN(phpClassName)) { Method method = phpClass.findMethodByName(methodName); if(method != null) { return method; } } return null; } static public boolean isMethodWithFirstString(PsiElement psiElement, String... methodName) { // filter out method calls without parameter // $this->methodName('service_name') // withName is not working, so simulate it in a hack if(!PlatformPatterns .psiElement(PhpElementTypes.METHOD_REFERENCE) .withChild(PlatformPatterns .psiElement(PhpElementTypes.PARAMETER_LIST) .withFirstChild(PlatformPatterns .psiElement(PhpElementTypes.STRING) ) ).accepts(psiElement)) { return false; } // cant we move it up to PlatformPatterns? withName condition dont looks working String methodRefName = ((MethodReference) psiElement).getName(); return null != methodRefName && Arrays.asList(methodName).contains(methodRefName); } /** * $this->methodName('service_name') * $this->methodName(SERVICE::NAME) * $this->methodName($this->name) */ static public boolean isMethodWithFirstStringOrFieldReference(PsiElement psiElement, String... methodName) { if(!PlatformPatterns .psiElement(PhpElementTypes.METHOD_REFERENCE) .withChild(PlatformPatterns .psiElement(PhpElementTypes.PARAMETER_LIST) .withFirstChild(PlatformPatterns.or( PlatformPatterns.psiElement(PhpElementTypes.STRING), PlatformPatterns.psiElement(PhpElementTypes.FIELD_REFERENCE), PlatformPatterns.psiElement(PhpElementTypes.CLASS_CONSTANT_REFERENCE) )) ).accepts(psiElement)) { return false; } // cant we move it up to PlatformPatterns? withName condition dont looks working String methodRefName = ((MethodReference) psiElement).getName(); return null != methodRefName && Arrays.asList(methodName).contains(methodRefName); } static public PsiElementPattern.Capture<StringLiteralExpression> methodWithFirstStringPattern() { return PlatformPatterns .psiElement(StringLiteralExpression.class) .withParent( PlatformPatterns.psiElement(PhpElementTypes.PARAMETER_LIST) .withFirstChild( PlatformPatterns.psiElement(PhpElementTypes.STRING) ) .withParent( PlatformPatterns.psiElement(PhpElementTypes.METHOD_REFERENCE) ) ) .withLanguage(PhpLanguage.INSTANCE); } /** * $foo->bar('<caret>') */ static public PsiElementPattern.Capture<PsiElement> getParameterInsideMethodReferencePattern() { return PlatformPatterns .psiElement() .withParent( PlatformPatterns.psiElement(StringLiteralExpression.class) .withParent( PlatformPatterns.psiElement(ParameterList.class) .withParent( PlatformPatterns.psiElement(MethodReference.class) ) ) ) .withLanguage(PhpLanguage.INSTANCE); } /** * class "Foo" extends */ static public PsiElementPattern.Capture<PsiElement> getClassNamePattern() { return PlatformPatterns .psiElement(PhpTokenTypes.IDENTIFIER) .afterLeafSkipping( PlatformPatterns.psiElement(PsiWhiteSpace.class), PlatformPatterns.psiElement(PhpTokenTypes.kwCLASS) ) .withParent(PhpClass.class) .withLanguage(PhpLanguage.INSTANCE); } /** * public function indexAction() */ static public PsiElementPattern.Capture<PsiElement> getActionMethodPattern() { return PlatformPatterns .psiElement(PhpTokenTypes.IDENTIFIER).withText( PlatformPatterns.string().endsWith("Action") ) .afterLeafSkipping( PlatformPatterns.psiElement(PsiWhiteSpace.class), PlatformPatterns.psiElement(PhpTokenTypes.kwFUNCTION) ) .inside(Method.class) .withLanguage(PhpLanguage.INSTANCE); } /** * return 'value' inside class method */ static public ElementPattern<PhpExpression> getMethodReturnPattern() { return PlatformPatterns.or( PlatformPatterns.psiElement(StringLiteralExpression.class) .withParent(PlatformPatterns.psiElement(PhpReturn.class).inside(Method.class)) .withLanguage(PhpLanguage.INSTANCE), PlatformPatterns.psiElement(ClassConstantReference.class) .withParent(PlatformPatterns.psiElement(PhpReturn.class).inside(Method.class)) .withLanguage(PhpLanguage.INSTANCE) ); } /** * Search for class with returns a string on a given method name */ @Nullable static public PhpClass findSubclassWithMethodReturnString(@NotNull Project project, @NotNull String subClass, @NotNull String methodName, @NotNull String returnString) { for (PhpClass phpClass : PhpIndex.getInstance(project).getAllSubclasses(subClass)) { if(returnString.equals(getMethodReturnAsString(phpClass, methodName))) { return phpClass; } } return null; } /** * Find a string return value of a method context "function() { return 'foo'}" * First match wins */ @Nullable static public String getMethodReturnAsString(@NotNull PhpClass phpClass, @NotNull String methodName) { final Collection<String> values = getMethodReturnAsStrings(phpClass, methodName); if(values.size() == 0) { return null; } // we support only first item return values.iterator().next(); } /** * Find a string return value of a method context "function() { return 'foo'}" */ @NotNull static public Collection<String> getMethodReturnAsStrings(@NotNull PhpClass phpClass, @NotNull String methodName) { Method method = phpClass.findMethodByName(methodName); if(method == null) { return Collections.emptyList(); } final Set<String> values = new HashSet<>(); method.acceptChildren(new PsiRecursiveElementWalkingVisitor() { @Override public void visitElement(PsiElement element) { if(PhpElementsUtil.getMethodReturnPattern().accepts(element)) { String value = PhpElementsUtil.getStringValue(element); if(value != null && StringUtils.isNotBlank(value)) { values.add(value); } } super.visitElement(element); } }); return values; } @Nullable static public PhpClass getClass(Project project, String className) { return getClass(PhpIndex.getInstance(project), className); } @Nullable static public PhpClass getClass(PhpIndex phpIndex, String className) { Collection<PhpClass> classes = phpIndex.getClassesByFQN(className); return classes.isEmpty() ? null : classes.iterator().next(); } @Nullable static public PhpClass getInterface(PhpIndex phpIndex, String className) { // api workaround if(!className.startsWith("\\")) { className = "\\" + className; } Collection<PhpClass> classes = phpIndex.getInterfacesByFQN(className); return classes.isEmpty() ? null : classes.iterator().next(); } @Nullable static public PhpClass getClassInterface(Project project, @NotNull String className) { // api workaround for at least interfaces if(!className.startsWith("\\")) { className = "\\" + className; } Collection<PhpClass> phpClasses = PhpIndex.getInstance(project).getAnyByFQN(className); return phpClasses.size() == 0 ? null : phpClasses.iterator().next(); } /** * @param subjectClass eg DateTime * @param expectedClass eg DateTimeInterface */ public static boolean isInstanceOf(@NotNull PhpClass subjectClass, @NotNull PhpClass expectedClass) { return new PhpType().add(expectedClass).isConvertibleFrom(new PhpType().add(subjectClass), PhpIndex.getInstance(subjectClass.getProject())); } /** * @param subjectClass eg DateTime * @param expectedClass eg DateTimeInterface */ public static boolean isInstanceOf(@NotNull PhpClass subjectClass, @NotNull String expectedClass) { return new PhpType().add(expectedClass).isConvertibleFrom(new PhpType().add(subjectClass), PhpIndex.getInstance(subjectClass.getProject())); } /** * @param subjectClass eg DateTime * @param expectedClass eg DateTimeInterface */ public static boolean isInstanceOf(@NotNull Project project, @NotNull String subjectClass, @NotNull String expectedClass) { return new PhpType().add(expectedClass).isConvertibleFrom(new PhpType().add(subjectClass), PhpIndex.getInstance(project)); } static public Collection<PhpClass> getClassesInterface(Project project, @NotNull String className) { // api workaround for at least interfaces if(!className.startsWith("\\")) { className = "\\" + className; } return PhpIndex.getInstance(project).getAnyByFQN(className); } static public void addClassPublicMethodCompletion(CompletionResultSet completionResultSet, PhpClass phpClass) { for(Method method: getClassPublicMethod(phpClass)) { completionResultSet.addElement(new PhpLookupElement(method)); } } static public ArrayList<Method> getClassPublicMethod(PhpClass phpClass) { ArrayList<Method> methods = new ArrayList<>(); for(Method method: phpClass.getMethods()) { if(method.getAccess().isPublic() && !method.getName().startsWith("__")) { methods.add(method); } } return methods; } @Nullable static public String getArrayHashValue(ArrayCreationExpression arrayCreationExpression, String keyName) { ArrayHashElement translationArrayHashElement = PsiElementUtils.getChildrenOfType(arrayCreationExpression, PlatformPatterns.psiElement(ArrayHashElement.class) .withFirstChild( PlatformPatterns.psiElement(PhpElementTypes.ARRAY_KEY).withText( PlatformPatterns.string().oneOf("'" + keyName + "'", "\"" + keyName + "\"") ) ) ); if(translationArrayHashElement == null) { return null; } if(!(translationArrayHashElement.getValue() instanceof StringLiteralExpression)) { return null; } StringLiteralExpression valueString = (StringLiteralExpression) translationArrayHashElement.getValue(); if(valueString == null) { return null; } return valueString.getContents(); } static public boolean isEqualMethodReferenceName(MethodReference methodReference, String methodName) { String name = methodReference.getName(); return name != null && name.equals(methodName); } static public PsiElement findArrayKeyValueInsideReference(PsiElement psiElement, String methodReferenceName, String keyName) { if(psiElement == null) { return null; } Collection<MethodReference> tests = PsiTreeUtil.findChildrenOfType(psiElement, MethodReference.class); for(MethodReference methodReference: tests) { // instance check // methodReference.getSignature().equals("#M#C\\Symfony\\Component\\OptionsResolver\\OptionsResolverInterface.setDefaults") if(PhpElementsUtil.isEqualMethodReferenceName(methodReference, methodReferenceName)) { PsiElement[] parameters = methodReference.getParameters(); if(parameters.length > 0 && parameters[0] instanceof ArrayCreationExpression) { PsiElement keyValue = PhpElementsUtil.getArrayValue((ArrayCreationExpression) parameters[0], keyName); if(keyValue != null) { return keyValue; } } } } return null; } @Nullable static public PsiElement getArrayKeyValueInsideSignaturePsi(PsiElement psiElementInsideClass, String callTo[], String methodName, String keyName) { PhpClass phpClass = PsiTreeUtil.getParentOfType(psiElementInsideClass, PhpClass.class); if(phpClass == null) { return null; } String className = phpClass.getPresentableFQN(); for (String s : callTo) { // @TODO: replace signature PsiElement arrayKeyValueInsideSignature = PhpElementsUtil.getArrayKeyValueInsideSignaturePsi(psiElementInsideClass.getProject(), "#M#C\\" + className + "." + s, methodName, keyName); if(arrayKeyValueInsideSignature != null) { return arrayKeyValueInsideSignature; } } return null; } @Nullable static public String getArrayKeyValueInsideSignature(PsiElement psiElementInsideClass, String callTo[], String methodName, String keyName) { return getStringValue(getArrayKeyValueInsideSignaturePsi(psiElementInsideClass, callTo, methodName, keyName)); } @Nullable static public PsiElement getArrayKeyValueInsideSignaturePsi(Project project, String signature, String methodName, String keyName) { PsiElement psiElement = PhpElementsUtil.getPsiElementsBySignatureSingle(project, signature); if(psiElement == null) { return null; } for(MethodReference methodReference: PsiTreeUtil.findChildrenOfType(psiElement, MethodReference.class)) { if(PhpElementsUtil.isEqualMethodReferenceName(methodReference, methodName)) { PsiElement[] parameters = methodReference.getParameters(); if(parameters.length > 0 && parameters[0] instanceof ArrayCreationExpression) { return PhpElementsUtil.getArrayValue((ArrayCreationExpression) parameters[0], keyName); } } } return null; } public static Method[] getImplementedMethods(@NotNull Method method) { ArrayList<Method> items = getImplementedMethods(method.getContainingClass(), method, new ArrayList<>()); return items.toArray(new Method[items.size()]); } private static ArrayList<Method> getImplementedMethods(@Nullable PhpClass phpClass, @NotNull Method method, ArrayList<Method> implementedMethods) { if (phpClass == null) { return implementedMethods; } Method[] methods = phpClass.getOwnMethods(); for (Method ownMethod : methods) { if (PhpLangUtil.equalsMethodNames(ownMethod.getName(), method.getName())) { implementedMethods.add(ownMethod); } } for(PhpClass interfaceClass: phpClass.getImplementedInterfaces()) { getImplementedMethods(interfaceClass, method, implementedMethods); } getImplementedMethods(phpClass.getSuperClass(), method, implementedMethods); return implementedMethods; } @Nullable public static String getStringValue(@Nullable PsiElement psiElement) { return getStringValue(psiElement, 0); } @Nullable private static String getStringValue(@Nullable PsiElement psiElement, int depth) { if(psiElement == null || ++depth > 5) { return null; } if(psiElement instanceof StringLiteralExpression) { String resolvedString = ((StringLiteralExpression) psiElement).getContents(); if(StringUtils.isEmpty(resolvedString)) { return null; } return resolvedString; } if(psiElement instanceof Field) { return getStringValue(((Field) psiElement).getDefaultValue(), depth); } if(psiElement instanceof PhpReference) { PsiReference psiReference = psiElement.getReference(); if(psiReference == null) { return null; } PsiElement ref = psiReference.resolve(); if(ref instanceof PhpReference) { return getStringValue(psiElement, depth); } if(ref instanceof Field) { PsiElement resolved = ((Field) ref).getDefaultValue(); if(resolved instanceof StringLiteralExpression) { return ((StringLiteralExpression) resolved).getContents(); } } } return null; } public static String getPrevSiblingAsTextUntil(PsiElement psiElement, ElementPattern pattern, boolean includeMatching) { String prevText = ""; for (PsiElement child = psiElement.getPrevSibling(); child != null; child = child.getPrevSibling()) { if(pattern.accepts(child)) { if(includeMatching) { return child.getText() + prevText; } return prevText; } else { prevText = child.getText() + prevText; } } return prevText; } public static String getPrevSiblingAsTextUntil(PsiElement psiElement, ElementPattern pattern) { return getPrevSiblingAsTextUntil(psiElement, pattern, false); } @Nullable public static ArrayCreationExpression getCompletableArrayCreationElement(PsiElement psiElement) { // array('<test>' => '') if(PhpPatterns.psiElement(PhpElementTypes.ARRAY_KEY).accepts(psiElement.getContext())) { PsiElement arrayKey = psiElement.getContext(); if(arrayKey != null) { PsiElement arrayHashElement = arrayKey.getContext(); if(arrayHashElement instanceof ArrayHashElement) { PsiElement arrayCreationExpression = arrayHashElement.getContext(); if(arrayCreationExpression instanceof ArrayCreationExpression) { return (ArrayCreationExpression) arrayCreationExpression; } } } } // on array creation key dont have value, so provide completion here also // array('foo' => 'bar', '<test>') if(PhpPatterns.psiElement(PhpElementTypes.ARRAY_VALUE).accepts(psiElement.getContext())) { PsiElement arrayKey = psiElement.getContext(); if(arrayKey != null) { PsiElement arrayCreationExpression = arrayKey.getContext(); if(arrayCreationExpression instanceof ArrayCreationExpression) { return (ArrayCreationExpression) arrayCreationExpression; } } } return null; } public static Collection<PhpClass> getClassFromPhpTypeSet(Project project, Set<String> types) { PhpType phpType = new PhpType(); for (String type : types) { phpType.add(type); } List<PhpClass> phpClasses = new ArrayList<>(); for(String typeName: PhpIndex.getInstance(project).completeType(project, phpType, new HashSet<>()).getTypes()) { if(typeName.startsWith("\\")) { PhpClass phpClass = PhpElementsUtil.getClassInterface(project, typeName); if(phpClass != null) { phpClasses.add(phpClass); } } } return phpClasses; } public static Collection<PhpClass> getClassFromPhpTypeSetArrayClean(Project project, Set<String> types) { PhpType phpType = new PhpType(); for (String type : types) { phpType.add(type); } ArrayList<PhpClass> phpClasses = new ArrayList<>(); for(String typeName: PhpIndex.getInstance(project).completeType(project, phpType, new HashSet<>()).getTypes()) { if(typeName.startsWith("\\")) { // we clean array types \Foo[] if(typeName.endsWith("[]")) { typeName = typeName.substring(0, typeName.length() - 2); } PhpClass phpClass = PhpElementsUtil.getClassInterface(project, typeName); if(phpClass != null) { phpClasses.add(phpClass); } } } return phpClasses; } @Nullable public static PhpClass getFirstClassFromFile(PhpFile phpFile) { Collection<PhpClass> phpClasses = PsiTreeUtil.collectElementsOfType(phpFile, PhpClass.class); return phpClasses.size() == 0 ? null : phpClasses.iterator().next(); } public static boolean isEqualClassName(@Nullable PhpClass phpClass, @Nullable String... compareClassNames) { for(String className: compareClassNames) { if(isEqualClassName(phpClass, className)) { return true; } } return false; } public static boolean isEqualClassName(@NotNull PhpClass phpClass, @NotNull PhpClass compareClassName) { return isEqualClassName(phpClass, compareClassName.getPresentableFQN()); } public static boolean isEqualClassName(@Nullable PhpClass phpClass, @Nullable String compareClassName) { if(phpClass == null || compareClassName == null) { return false; } return StringUtils.stripStart(phpClass.getPresentableFQN(), "\\") .equals(StringUtils.stripStart(compareClassName, "\\")); } @Nullable public static PsiElement[] getMethodParameterReferences(Method method, int parameterIndex) { // we dont have a parameter on resolved method Parameter[] parameters = method.getParameters(); if(parameters.length == 0 || parameterIndex >= parameters.length) { return null; } final String tempVariableName = parameters[parameterIndex].getName(); return PsiTreeUtil.collectElements(method.getLastChild(), element -> element instanceof Variable && tempVariableName.equals(((Variable) element).getName()) ); } @Nullable public static MethodReferenceBag getMethodParameterReferenceBag(PsiElement psiElement, int wantIndex) { PsiElement variableContext = psiElement.getContext(); if(!(variableContext instanceof ParameterList)) { return null; } ParameterList parameterList = (ParameterList) variableContext; if (!(parameterList.getContext() instanceof MethodReference)) { return null; } ParameterBag currentIndex = PsiElementUtils.getCurrentParameterIndex(psiElement); if(currentIndex == null) { return null; } if(wantIndex >= 0 && currentIndex.getIndex() != wantIndex) { return null; } return new MethodReferenceBag(parameterList, (MethodReference) parameterList.getContext(), currentIndex); } @Nullable public static MethodReferenceBag getMethodParameterReferenceBag(PsiElement psiElement) { return getMethodParameterReferenceBag(psiElement, -1); } /** * Find all variables in current function / method scope * * $v<caret>ar = 'foobar'; * $v<caret>ar->foo() */ public static Collection<Variable> getVariableReferencesInScope(@NotNull Variable variable) { Variable variableDecl = null; if(!variable.isDeclaration()) { PsiElement psiElement = variable.resolve(); if(psiElement instanceof Variable) { variableDecl = (Variable) psiElement; } } else { variableDecl = variable; } if(variableDecl == null) { return Collections.emptyList(); } Function function = PsiTreeUtil.getParentOfType(variable, Function.class); if(function == null) { return Collections.emptyList(); } final List<Variable> variables = new ArrayList<>(); for (Variable variableRef : PhpElementsUtil.getVariablesInScope(function, variableDecl)) { if (!variableRef.equals(variable)) { variables.add(variableRef); } } return variables; } /** * Try to visit possible class name for PsiElements with text like "Foo\|Bar", "Foo|\Bar", "\Foo|\Bar" * Cursor must have position in PsiElement * * @param psiElement the element context, cursor should be in it * @param cursorOffset current cursor editor eg from completion context * @param visitor callback on matching class */ public static void visitNamespaceClassForCompletion(PsiElement psiElement, int cursorOffset, ClassForCompletionVisitor visitor) { int cursorOffsetClean = cursorOffset - psiElement.getTextOffset(); if(cursorOffsetClean < 1) { return; } String content = psiElement.getText(); int length = content.length(); if(!(length >= cursorOffsetClean)) { return; } String beforeCursor = content.substring(0, cursorOffsetClean); boolean isValid; // espend\|Container, espend\Cont|ainer <- fallback to last full namespace // espend|\Container <- only on known namespace "espend" String namespace = beforeCursor; // if no backslash or its equal in first position, fallback on namespace completion int lastSlash = beforeCursor.lastIndexOf("\\"); if(lastSlash <= 0) { isValid = PhpIndexUtil.hasNamespace(psiElement.getProject(), beforeCursor); } else { isValid = true; namespace = beforeCursor.substring(0, lastSlash); } if(!isValid) { return; } // format namespaces and add prefix for fluent completion String prefix = ""; if(namespace.startsWith("\\")) { prefix = "\\"; } else { namespace = "\\" + namespace; } // search classes in current namespace and child namespaces for(PhpClass phpClass: PhpIndexUtil.getPhpClassInsideNamespace(psiElement.getProject(), namespace)) { String presentableFQN = phpClass.getPresentableFQN(); if(fr.adrienbrault.idea.symfony2plugin.util.StringUtils.startWithEqualClassname(presentableFQN, beforeCursor)) { visitor.visit(phpClass, presentableFQN, prefix); } } } public interface ClassForCompletionVisitor { void visit(PhpClass phpClass, String presentableFQN, String prefix); } /** * new FooClass() */ @Nullable private static PhpClass getNewExpressionPhpClass(@NotNull NewExpression newExpression) { ClassReference classReference = newExpression.getClassReference(); if(classReference != null) { String fqn = classReference.getFQN(); if(fqn != null) { return PhpElementsUtil.getClass(newExpression.getProject(), fqn); } } return null; } /** * Get PhpClass from "new FooClass()" only if match instance condition */ public static PhpClass getNewExpressionPhpClassWithInstance(@NotNull NewExpression newExpression, @NotNull String instance) { PhpClass phpClass = getNewExpressionPhpClass(newExpression); if(phpClass != null && PhpElementsUtil.isInstanceOf(phpClass, instance)) { return phpClass; } return null; } public static boolean isNewExpressionPhpClassWithInstance(@NotNull NewExpression newExpression, @NotNull String instance) { return getNewExpressionPhpClassWithInstance(newExpression, instance) != null; } @NotNull public static Collection<PsiElement> collectMethodElementsWithParents(final @NotNull Method method, @NotNull final Processor<PsiElement> processor) { Collection<PsiElement> elements = new HashSet<>(); collectMethodElementsWithParents(method, 3, elements, processor); return elements; } private static void collectMethodElementsWithParents(final @NotNull Method method, final int depth, @NotNull final Collection<PsiElement> elements, @NotNull final Processor<PsiElement> processor) { method.acceptChildren(new PsiRecursiveElementWalkingVisitor() { @Override public void visitElement(PsiElement psiElement) { if (processor.process(psiElement)) { elements.add(psiElement); } if (psiElement instanceof MethodReference && ((MethodReference) psiElement).getReferenceType() == PhpModifier.State.PARENT && method.getName().equals(((MethodReference) psiElement).getName())) { PsiElement resolve = ((MethodReference) psiElement).resolve(); if (depth > 0 && resolve instanceof Method) { collectMethodElementsWithParents((Method) resolve, depth - 1, elements, processor); } } super.visitElement(psiElement); } }); } /** * Gets parameter which are non optional and at the end of a function signature * * foo($container, $bar = null, $foo = null); * */ @NotNull public static Parameter[] getFunctionRequiredParameter(@NotNull Function function) { // nothing we need to do Parameter[] parameters = function.getParameters(); if(parameters.length == 0) { return new Parameter[0]; } // find last optional parameter int last = -1; for (int i = parameters.length - 1; i >= 0; i--) { if(!parameters[i].isOptional()) { last = i; break; } } // no required argument found if(last == -1) { return new Parameter[0]; } return Arrays.copyOfRange(parameters, 0, last + 1); } public static boolean isTestClass(@NotNull PhpClass phpClass) { if(PhpUnitUtil.isTestClass(phpClass)) { return true; } String fqn = phpClass.getPresentableFQN(); return fqn.contains("\\Test\\") || fqn.contains("\\Tests\\"); } /** * Extract type hint from method parameter * * function foo(\FooClass $class) */ @Nullable public static String getMethodParameterTypeHint(@NotNull Method method) { ParameterList childOfType = PsiTreeUtil.getChildOfType(method, ParameterList.class); if(childOfType == null) { return null; } PsiElement[] parameters = childOfType.getParameters(); if(parameters.length == 0) { return null; } ClassReference classReference = PsiTreeUtil.getChildOfType(parameters[0], ClassReference.class); if(classReference == null) { return null; } String fqn = classReference.getFQN(); if(fqn == null) { return null; } return fqn; } /** * "DateTime", DateTime::class */ @Nullable public static PhpClass resolvePhpClassOnPsiElement(@NotNull PsiElement psiElement) { String dataClass = null; if(psiElement instanceof ClassConstantReference) { PsiElement lastChild = psiElement.getLastChild(); // @TODO: FOO::class find PhpElementTyp: toString provides "class" if("class".equals(lastChild.getText())) { PhpExpression classReference = ((ClassConstantReference) psiElement).getClassReference(); if(classReference instanceof PhpReference) { dataClass = ((PhpReference) classReference).getFQN(); } } } else { dataClass = getStringValue(psiElement); } if(dataClass == null) { return null; } return getClassInterface(psiElement.getProject(), dataClass); } /** * Find first variable declaration in parent scope of a given variable: * * function() { * $event = new FooEvent(); * dispatch('foo', $event); * } */ @Nullable public static String getFirstVariableTypeInScope(@NotNull Variable variable) { // parent search scope, eg Method else fallback to a grouped statement PsiElement searchScope = PsiTreeUtil.getParentOfType(variable, Function.class); if(searchScope == null) { searchScope = PsiTreeUtil.getParentOfType(variable, GroupStatement.class); } if(searchScope == null) { return null; } final String name = variable.getName(); if(name == null) { return null; } final String[] result = {null}; searchScope.acceptChildren(new PsiRecursiveElementVisitor() { @Override public void visitElement(PsiElement element) { if(element instanceof Variable && name.equals(((Variable) element).getName())) { PsiElement assignmentExpression = element.getParent(); if(assignmentExpression instanceof AssignmentExpression) { PhpPsiElement value = ((AssignmentExpression) assignmentExpression).getValue(); if(value instanceof NewExpression) { ClassReference classReference = ((NewExpression) value).getClassReference(); if(classReference != null) { String classSignature = classReference.getFQN(); if(StringUtils.isNotBlank(classSignature)) { result[0] = classSignature; } } } } } super.visitElement(element); } }); return result[0]; } /** * Get class by shortcut namespace, on a scoped namespace * @param project current project * @param classNameScope Namespace fo search "\Foo\Foo", "Foo\Foo", "Foo\Foo\", last "\*" is stripped * @param className Class name inside namespace also fqn is supported * @return PhpClass matched */ public static PhpClass getClassInsideNamespaceScope(@NotNull Project project, @NotNull String classNameScope, @NotNull String className) { if(className.startsWith("\\")) { return PhpElementsUtil.getClassInterface(project, className); } // strip class name we namespace String strip = StringUtils.strip(classNameScope, "\\"); int i = strip.lastIndexOf("\\"); if(i <= 0) { return PhpElementsUtil.getClassInterface(project, className); } PhpClass phpClass = PhpElementsUtil.getClassInterface(project, strip.substring(0, i) + "\\" + StringUtils.strip(className, "\\")); if(phpClass != null) { return phpClass; } return PhpElementsUtil.getClassInterface(project, className); } /** * Resolves MethodReference and compare containing class against implementations instances */ public static boolean isMethodReferenceInstanceOf(@NotNull MethodReference methodReference, @NotNull String expectedClassName) { for (ResolveResult resolveResult : methodReference.multiResolve(false)) { PsiElement resolve = resolveResult.getElement(); if(!(resolve instanceof Method)) { continue; } PhpClass containingClass = ((Method) resolve).getContainingClass(); if(containingClass == null) { continue; } if(!PhpElementsUtil.isInstanceOf(containingClass, expectedClassName)) { continue; } return true; } return false; } /** * Resolves MethodReference and compare containing class against implementations instances */ public static boolean isMethodReferenceInstanceOf(@NotNull MethodReference methodReference, @NotNull String expectedClassName, @NotNull String methodName) { if(!methodName.equals(methodReference.getName())) { return false; } return isMethodReferenceInstanceOf(methodReference, expectedClassName); } public static void replaceElementWithClassConstant(@NotNull PhpClass phpClass, @NotNull PsiElement originElement) throws Exception{ String fqn = phpClass.getFQN(); if(fqn == null) { throw new Exception("Class fqn empty"); } if(!fqn.startsWith("\\")) { fqn = "\\" + fqn; } PhpPsiElement scopeForUseOperator = PhpCodeInsightUtil.findScopeForUseOperator(originElement); if(scopeForUseOperator == null) { throw new Exception("Class fqn error"); } if(!PhpCodeInsightUtil.getAliasesInScope(scopeForUseOperator).values().contains(fqn)) { PhpAliasImporter.insertUseStatement(fqn, scopeForUseOperator); } originElement.replace(PhpPsiElementFactory.createPhpPsiFromText( originElement.getProject(), ClassConstantReference.class, "<?php " + phpClass.getName() + "::class" )); } /** * add('', <caret>), add('', Foo<caret>) */ @Nullable public static MethodReference findMethodReferenceOnClassConstant(PsiElement psiElement) { PsiElement parameterList = psiElement.getParent(); if(parameterList instanceof ParameterList) { PsiElement psiElement2 = parameterList.getParent(); if(psiElement2 instanceof MethodReference) { return (MethodReference) psiElement2; } } else if(parameterList instanceof ConstantReference) { PsiElement parent = parameterList.getParent(); if(parent instanceof ParameterList) { return PsiElementAssertUtil.getParentOfTypeOrNull(parent, MethodReference.class); } } return null; } /** * Foo::class to its PhpClass */ public static PhpClass getClassConstantPhpClass(@NotNull ClassConstantReference classConstant) { String typeName = getClassConstantPhpFqn(classConstant); return typeName != null ? PhpElementsUtil.getClassInterface(classConstant.getProject(), typeName) : null; } /** * Foo::class to its class fqn include namespace */ public static String getClassConstantPhpFqn(@NotNull ClassConstantReference classConstant) { PhpExpression classReference = classConstant.getClassReference(); if(!(classReference instanceof PhpReference)) { return null; } String typeName = ((PhpReference) classReference).getFQN(); return typeName != null && StringUtils.isNotBlank(typeName) ? StringUtils.stripStart(typeName, "\\") : null; } /** * Get type hint PhpClass of an given method index * * @param parameterIndex staring 0 */ @Nullable public static PhpClass getMethodTypeHintParameterPhpClass(@NotNull Method method, int parameterIndex) { Parameter[] constructorParameter = method.getParameters(); if(parameterIndex >= constructorParameter.length) { return null; } String className = constructorParameter[parameterIndex].getDeclaredType().toString(); if(StringUtils.isBlank(className)) { return null; } return PhpElementsUtil.getClassInterface(method.getProject(), className); } @Nullable public static String insertUseIfNecessary(@NotNull PsiElement phpClass, @NotNull String fqnClasName) { if(!fqnClasName.startsWith("\\")) { fqnClasName = "\\" + fqnClasName; } PhpPsiElement scopeForUseOperator = PhpCodeInsightUtil.findScopeForUseOperator(phpClass); if(scopeForUseOperator == null) { return null; } if(!PhpCodeInsightUtil.getAliasesInScope(scopeForUseOperator).values().contains(fqnClasName)) { PhpAliasImporter.insertUseStatement(fqnClasName, scopeForUseOperator); } for (Map.Entry<String, String> entry : PhpCodeInsightUtil.getAliasesInScope(scopeForUseOperator).entrySet()) { if(fqnClasName.equals(entry.getValue())) { return entry.getKey(); } } return null; } /** * Collects all variables in a given scope. * Eg find all variables usages in a given method */ @NotNull public static Set<Variable> getVariablesInScope(@NotNull PsiElement psiElement, @NotNull PhpNamedElement variable) { return MyVariableRecursiveElementVisitor.visit(psiElement, variable.getName()); } @NotNull public static Set<Variable> getVariablesInScope(@NotNull PsiElement psiElement, @NotNull String name) { return MyVariableRecursiveElementVisitor.visit(psiElement, name); } /** * Provide array key pattern. we need incomplete array key support, too. * * foo(['<caret>']) * foo(['<caret>' => 'foobar']) */ @NotNull public static PsiElementPattern.Capture<PsiElement> getParameterListArrayValuePattern() { return PlatformPatterns.psiElement() .withParent(PlatformPatterns.psiElement(StringLiteralExpression.class).withParent( PlatformPatterns.or( PlatformPatterns.psiElement().withElementType(PhpElementTypes.ARRAY_VALUE) .withParent(PlatformPatterns.psiElement(ArrayCreationExpression.class) .withParent(ParameterList.class) ), PlatformPatterns.psiElement().withElementType(PhpElementTypes.ARRAY_KEY) .withParent(PlatformPatterns.psiElement(ArrayHashElement.class) .withParent(PlatformPatterns.psiElement(ArrayCreationExpression.class) .withParent(ParameterList.class) ) ) )) ); } /** * Visit and collect all variables in given scope */ private static class MyVariableRecursiveElementVisitor extends PsiRecursiveElementVisitor { @NotNull private final String name; @NotNull private final Set<Variable> variables = new HashSet<>(); MyVariableRecursiveElementVisitor(@NotNull String name) { this.name = name; } @Override public void visitElement(PsiElement element) { if(element instanceof Variable && name.equals(((Variable) element).getName())) { variables.add((Variable) element); } super.visitElement(element); } public static Set<Variable> visit(@NotNull PsiElement scope, @NotNull String name) { MyVariableRecursiveElementVisitor visitor = new MyVariableRecursiveElementVisitor(name); scope.acceptChildren(visitor); return visitor.variables; } } }