package fr.adrienbrault.idea.symfony2plugin.util;
import com.intellij.patterns.PlatformPatterns;
import com.intellij.patterns.PsiElementPattern;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiWhiteSpace;
import com.intellij.psi.util.PsiTreeUtil;
import com.intellij.util.containers.ContainerUtil;
import com.jetbrains.php.codeInsight.PhpCodeInsightUtil;
import com.jetbrains.php.lang.PhpLangUtil;
import com.jetbrains.php.lang.documentation.phpdoc.PhpDocUtil;
import com.jetbrains.php.lang.documentation.phpdoc.lexer.PhpDocTokenTypes;
import com.jetbrains.php.lang.documentation.phpdoc.parser.PhpDocElementTypes;
import com.jetbrains.php.lang.documentation.phpdoc.psi.PhpDocComment;
import com.jetbrains.php.lang.documentation.phpdoc.psi.tags.PhpDocTag;
import com.jetbrains.php.lang.psi.PhpPsiUtil;
import com.jetbrains.php.lang.psi.elements.*;
import org.apache.commons.lang.StringUtils;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* Some method from Php Annotations plugin to not fully set a "depends" entry on it
*
* @author Daniel Espendiller <daniel@espendiller.net>
*/
public class AnnotationBackportUtil {
public static Set<String> NON_ANNOTATION_TAGS = new HashSet<String>() {{
addAll(Arrays.asList(PhpDocUtil.ALL_TAGS));
add("@Annotation");
add("@inheritDoc");
add("@Enum");
add("@inheritdoc");
add("@Target");
}};
@Nullable
public static PhpClass getAnnotationReference(PhpDocTag phpDocTag, final Map<String, String> useImports) {
String tagName = phpDocTag.getName();
if(tagName.startsWith("@")) {
tagName = tagName.substring(1);
}
String className = tagName;
String subNamespaceName = "";
if(className.contains("\\")) {
className = className.substring(0, className.indexOf("\\"));
subNamespaceName = tagName.substring(className.length());
}
if(!useImports.containsKey(className)) {
return null;
}
return PhpElementsUtil.getClass(phpDocTag.getProject(), useImports.get(className) + subNamespaceName);
}
@NotNull
public static Map<String, String> getUseImportMap(@NotNull PhpDocTag phpDocTag) {
PhpDocComment phpDoc = PsiTreeUtil.getParentOfType(phpDocTag, PhpDocComment.class);
if(phpDoc == null) {
return Collections.emptyMap();
}
return getUseImportMap(phpDoc);
}
@NotNull
public static Map<String, String> getUseImportMap(@NotNull PhpDocComment phpDocComment) {
// search for use alias in local file
final Map<String, String> useImports = new HashMap<>();
PhpPsiElement scope = PhpCodeInsightUtil.findScopeForUseOperator(phpDocComment);
if(scope == null) {
return useImports;
}
for (PhpUseList phpUseList : PhpCodeInsightUtil.collectImports(scope)) {
for (PhpUse phpUse : phpUseList.getDeclarations()) {
String alias = phpUse.getAliasName();
if (alias != null) {
useImports.put(alias, phpUse.getFQN());
} else {
useImports.put(phpUse.getName(), phpUse.getFQN());
}
}
}
return useImports;
}
@NotNull
public static Collection<PhpDocTag> filterValidDocTags(Collection<PhpDocTag> phpDocTags) {
Collection<PhpDocTag> filteredPhpDocTags = new ArrayList<>();
for(PhpDocTag phpDocTag: phpDocTags) {
if(!NON_ANNOTATION_TAGS.contains(phpDocTag.getName())) {
filteredPhpDocTags.add(phpDocTag);
}
}
return filteredPhpDocTags;
}
public static boolean hasReference(@Nullable PhpDocComment docComment, String... className) {
if(docComment == null) return false;
Map<String, String> uses = AnnotationBackportUtil.getUseImportMap(docComment);
for(PhpDocTag phpDocTag: PsiTreeUtil.findChildrenOfAnyType(docComment, PhpDocTag.class)) {
if(!AnnotationBackportUtil.NON_ANNOTATION_TAGS.contains(phpDocTag.getName())) {
PhpClass annotationReference = AnnotationBackportUtil.getAnnotationReference(phpDocTag, uses);
if(annotationReference != null && PhpElementsUtil.isEqualClassName(annotationReference, className)) {
return true;
}
}
}
return false;
}
public static PhpDocTag getReference(@Nullable PhpDocComment docComment, String className) {
if(docComment == null) return null;
Map<String, String> uses = AnnotationBackportUtil.getUseImportMap(docComment);
for(PhpDocTag phpDocTag: PsiTreeUtil.findChildrenOfAnyType(docComment, PhpDocTag.class)) {
if(AnnotationBackportUtil.NON_ANNOTATION_TAGS.contains(phpDocTag.getName())) {
continue;
}
PhpClass annotationReference = AnnotationBackportUtil.getAnnotationReference(phpDocTag, uses);
if(annotationReference != null && PhpElementsUtil.isEqualClassName(annotationReference, className)) {
return phpDocTag;
}
}
return null;
}
/**
* Use AnnotationUtil of "PHP Annotations Plugin"
*/
@Nullable
@Deprecated
public static String getAnnotationRouteName(@Nullable String rawDocText) {
if(rawDocText == null) {
return null;
}
Matcher matcher = Pattern.compile("name\\s*=\\s*\"([\\w\\.-]+)\"").matcher(rawDocText);
if (matcher.find()) {
return matcher.group(1);
}
return null;
}
/**
* Get class path on "use" path statement
*/
@Nullable
public static String getQualifiedName(@NotNull PsiElement psiElement, @NotNull String fqn) {
PhpPsiElement scopeForUseOperator = PhpCodeInsightUtil.findScopeForUseOperator(psiElement);
if (scopeForUseOperator == null) {
return null;
}
PhpReference reference = PhpPsiUtil.getParentByCondition(psiElement, false, PhpReference.INSTANCEOF);
String qualifiedName = PhpCodeInsightUtil.createQualifiedName(scopeForUseOperator, fqn, reference, false);
if (!PhpLangUtil.isFqn(qualifiedName)) {
return qualifiedName;
}
// @TODO: remove full fqn fallback
if(qualifiedName.startsWith("\\")) {
qualifiedName = qualifiedName.substring(1);
}
return qualifiedName;
}
/**
* "AppBundle\Controller\DefaultController::fooAction" => app_default_foo"
* "Foo\ParkResortBundle\Controller\SubController\BundleController\FooController::nestedFooAction" => foo_parkresort_sub_bundle_foo_nestedfoo"
*/
public static String getRouteByMethod(@NotNull PhpDocTag phpDocTag) {
PhpPsiElement method = getMethodScope(phpDocTag);
if (method == null) {
return null;
}
String name = method.getName();
if(name == null) {
return null;
}
// strip action
if(name.endsWith("Action")) {
name = name.substring(0, name.length() - "Action".length());
}
PhpClass containingClass = ((Method) method).getContainingClass();
if(containingClass == null) {
return null;
}
String[] fqn = org.apache.commons.lang.StringUtils.strip(containingClass.getFQN(), "\\").split("\\\\");
// remove empty and controller only namespace
List<String> filter = ContainerUtil.filter(fqn, s ->
org.apache.commons.lang.StringUtils.isNotBlank(s) && !"controller".equalsIgnoreCase(s)
);
if(filter.size() == 0) {
return null;
}
return org.apache.commons.lang.StringUtils.join(ContainerUtil.map(filter, s -> {
String content = s.toLowerCase();
if (content.endsWith("bundle") && !content.equalsIgnoreCase("bundle")) {
return content.substring(0, content.length() - "bundle".length());
}
if (content.endsWith("controller") && !content.equalsIgnoreCase("controller")) {
return content.substring(0, content.length() - "controller".length());
}
return content;
}), "_") + (!name.startsWith("_") ? "_" : "") + name.toLowerCase();
}
@Nullable
public static Method getMethodScope(@NotNull PhpDocTag phpDocTag) {
PhpDocComment parentOfType = PsiTreeUtil.getParentOfType(phpDocTag, PhpDocComment.class);
if(parentOfType == null) {
return null;
}
PhpPsiElement method = parentOfType.getNextPsiSibling();
if(!(method instanceof Method)) {
return null;
}
return (Method) method;
}
/**
* "@Template("foobar.html.twig")"
* "@Template(template="foobar.html.twig")"
*/
@Nullable
public static String getDefaultOrPropertyContents(@NotNull PhpDocTag phpDocTag, @NotNull String property) {
PhpPsiElement attributeList = phpDocTag.getFirstPsiChild();
if(attributeList == null || attributeList.getNode().getElementType() != PhpDocElementTypes.phpDocAttributeList) {
return null;
}
String contents = null;
PsiElement lParen = attributeList.getFirstChild();
if(lParen == null) {
return null;
}
PsiElement defaultValue = lParen.getNextSibling();
if(defaultValue instanceof StringLiteralExpression) {
// @Template("foobar.html.twig")
contents = ((StringLiteralExpression) defaultValue).getContents();
} else {
// @Template(template="foobar.html.twig")
PsiElement psiProperty = ContainerUtil.find(attributeList.getChildren(), psiElement ->
getPropertyIdentifierValue(property).accepts(psiElement)
);
if(psiProperty instanceof StringLiteralExpression) {
contents = ((StringLiteralExpression) psiProperty).getContents();
}
}
if(StringUtils.isNotBlank(contents)) {
return contents;
}
return contents;
}
/**
* matches "@Callback(propertyName="<value>")"
*/
private static PsiElementPattern.Capture<StringLiteralExpression> getPropertyIdentifierValue(String propertyName) {
return PlatformPatterns.psiElement(StringLiteralExpression.class)
.afterLeafSkipping(
PlatformPatterns.or(
PlatformPatterns.psiElement(PsiWhiteSpace.class),
PlatformPatterns.psiElement(PhpDocTokenTypes.DOC_TEXT).withText("=")
),
PlatformPatterns.psiElement(PhpDocTokenTypes.DOC_IDENTIFIER).withText(propertyName)
)
.withParent(PlatformPatterns.psiElement(PhpDocElementTypes.phpDocAttributeList));
}
}