package fr.adrienbrault.idea.symfony2plugin.config;
import com.intellij.codeInsight.lookup.LookupElement;
import com.intellij.codeInsight.lookup.LookupElementBuilder;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.Key;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.patterns.PlatformPatterns;
import com.intellij.psi.PsiElement;
import com.intellij.psi.search.GlobalSearchScope;
import com.intellij.psi.util.*;
import com.intellij.psi.xml.XmlAttribute;
import com.intellij.psi.xml.XmlFile;
import com.intellij.psi.xml.XmlTag;
import com.intellij.util.containers.ContainerUtil;
import com.intellij.util.containers.HashMap;
import com.intellij.util.indexing.FileBasedIndex;
import com.jetbrains.php.PhpIndex;
import com.jetbrains.php.lang.parser.PhpElementTypes;
import com.jetbrains.php.lang.psi.PhpFile;
import com.jetbrains.php.lang.psi.elements.*;
import fr.adrienbrault.idea.symfony2plugin.Symfony2Icons;
import fr.adrienbrault.idea.symfony2plugin.config.dic.EventDispatcherSubscribedEvent;
import fr.adrienbrault.idea.symfony2plugin.dic.XmlEventParser;
import fr.adrienbrault.idea.symfony2plugin.stubs.ContainerCollectionResolver;
import fr.adrienbrault.idea.symfony2plugin.stubs.cache.FileIndexCaches;
import fr.adrienbrault.idea.symfony2plugin.stubs.dict.DispatcherEvent;
import fr.adrienbrault.idea.symfony2plugin.stubs.indexes.EventAnnotationStubIndex;
import fr.adrienbrault.idea.symfony2plugin.util.EventSubscriberUtil;
import fr.adrienbrault.idea.symfony2plugin.util.PhpElementsUtil;
import fr.adrienbrault.idea.symfony2plugin.util.PsiElementUtils;
import fr.adrienbrault.idea.symfony2plugin.util.service.ServiceXmlParserFactory;
import fr.adrienbrault.idea.symfony2plugin.util.yaml.YamlHelper;
import org.apache.commons.lang.StringUtils;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.yaml.psi.YAMLFile;
import org.jetbrains.yaml.psi.YAMLKeyValue;
import org.jetbrains.yaml.psi.YAMLMapping;
import java.util.*;
/**
* @author Daniel Espendiller <daniel@espendiller.net>
*/
public class EventDispatcherSubscriberUtil {
private static final Key<CachedValue<Collection<EventDispatcherSubscribedEvent>>> EVENT_SUBSCRIBERS = new Key<>("SYMFONY_EVENT_SUBSCRIBERS");
private static final Key<CachedValue<Set<String>>> EVENT_ANNOTATIONS = new Key<>("SYMFONY_EVENT_ANNOTATIONS");
@NotNull
public static Collection<EventDispatcherSubscribedEvent> getSubscribedEvents(final @NotNull Project project) {
CachedValue<Collection<EventDispatcherSubscribedEvent>> cache = project.getUserData(EVENT_SUBSCRIBERS);
if (cache == null) {
cache = CachedValuesManager.getManager(project).createCachedValue(() ->
CachedValueProvider.Result.create(getSubscribedEventsProxy(project), PsiModificationTracker.MODIFICATION_COUNT), false
);
project.putUserData(EVENT_SUBSCRIBERS, cache);
}
return cache.getValue();
}
@NotNull
private static Collection<EventDispatcherSubscribedEvent> getSubscribedEventsProxy(@NotNull Project project) {
Collection<EventDispatcherSubscribedEvent> events = new ArrayList<>();
// http://symfony.com/doc/current/components/event_dispatcher/introduction.html
PhpIndex phpIndex = PhpIndex.getInstance(project);
Collection<PhpClass> phpClasses = phpIndex.getAllSubclasses("\\Symfony\\Component\\EventDispatcher\\EventSubscriberInterface");
for(PhpClass phpClass: phpClasses) {
if(PhpElementsUtil.isTestClass(phpClass)) {
continue;
}
Method method = phpClass.findMethodByName("getSubscribedEvents");
if(method != null) {
PhpReturn phpReturn = PsiTreeUtil.findChildOfType(method, PhpReturn.class);
if(phpReturn != null) {
attachSubscriberEventNames(events, phpClass, phpReturn);
}
}
}
return events;
}
private static void attachSubscriberEventNames(@NotNull Collection<EventDispatcherSubscribedEvent> events, @NotNull PhpClass phpClass, @NotNull PhpReturn phpReturn) {
PhpPsiElement array = phpReturn.getFirstPsiChild();
if(!(array instanceof ArrayCreationExpression)) {
return;
}
String presentableFQN = phpClass.getPresentableFQN();
Iterable<ArrayHashElement> arrayHashElements = ((ArrayCreationExpression) array).getHashElements();
for(ArrayHashElement arrayHashElement: arrayHashElements) {
PsiElement arrayKey = arrayHashElement.getKey();
PsiElement value = null;
// get method name
// @TODO: support multiple method names, currently we only use method name if type hint, so first item helps for now
Collection<PsiElement> subscriberMethods = getSubscriberMethods(arrayHashElement);
if(subscriberMethods.size() > 0) {
value = subscriberMethods.iterator().next();
}
if(arrayKey instanceof StringLiteralExpression) {
// ['doh' => 'method']
events.add(new EventDispatcherSubscribedEvent(
((StringLiteralExpression) arrayKey).getContents(),
presentableFQN,
PhpElementsUtil.getStringValue(value)
));
} else if(arrayKey instanceof PhpReference) {
String resolvedString = PhpElementsUtil.getStringValue(arrayKey);
if(resolvedString != null) {
// [FOO::BAR => 'method']
events.add(new EventDispatcherSubscribedEvent(
resolvedString,
presentableFQN,
PhpElementsUtil.getStringValue(value),
((PhpReference) arrayKey).getSignature())
);
}
}
}
}
/**
* Extract method name for subscribe
*
* 'pre.foo1' => 'foo'
* 'pre.foo1' => ['onStoreOrder', 0]
* 'pre.foo2' => [['onStoreOrder', 0]]
*/
@NotNull
private static Collection<PsiElement> getSubscriberMethods(@NotNull ArrayHashElement arrayHashElement) {
// support string, constants and array values
PhpPsiElement value = arrayHashElement.getValue();
if(value == null) {
Collections.emptySet();
}
// 'pre.foo' => [...]
if(!(value instanceof ArrayCreationExpression)) {
return new ArrayList<>(Collections.singletonList(value));
}
Collection<PsiElement> psiElements = new HashSet<>();
// 'pre.foo' => [<caret>]
PsiElement firstChild = value.getFirstPsiChild();
if(firstChild != null && firstChild.getNode().getElementType() == PhpElementTypes.ARRAY_VALUE) {
PhpPsiElement firstPsiChild = ((PhpPsiElement) firstChild).getFirstPsiChild();
if(firstPsiChild instanceof StringLiteralExpression) {
// 'pre.foo' => ['method']
psiElements.add(firstPsiChild);
} else if(firstPsiChild instanceof ArrayCreationExpression) {
// 'pre.foo' => [['method', ...], ['method2', ...]]
for (PsiElement psiElement : PsiElementUtils.getChildrenOfTypeAsList(firstPsiChild, PlatformPatterns.psiElement().withElementType(PhpElementTypes.ARRAY_VALUE))) {
if(!(psiElement instanceof PhpPsiElement)) {
continue;
}
PhpPsiElement prioValue = ((PhpPsiElement) psiElement).getFirstPsiChild();
if(prioValue instanceof StringLiteralExpression) {
psiElements.add(prioValue);
}
}
}
}
return psiElements;
}
@NotNull
public static Collection<EventDispatcherSubscribedEvent> getSubscribedEvent(@NotNull Project project, @NotNull String eventName) {
List<EventDispatcherSubscribedEvent> events = new ArrayList<>();
for(EventDispatcherSubscribedEvent event: getSubscribedEvents(project)) {
if(event.getStringValue().equals(eventName)) {
events.add(event);
}
}
return events;
}
@NotNull
public static Collection<PsiElement> getEventPsiElements(@NotNull final Project project, final @NotNull String eventName) {
final Collection<PsiElement> psiElements = new HashSet<>();
// @TODO: remove
XmlEventParser xmlEventParser = ServiceXmlParserFactory.getInstance(project, XmlEventParser.class);
for(EventDispatcherSubscribedEvent event : xmlEventParser.getEventSubscribers(eventName)) {
PhpClass phpClass = PhpElementsUtil.getClass(project, event.getFqnClassName());
if(phpClass != null) {
psiElements.add(phpClass);
}
}
for(EventDispatcherSubscribedEvent event: EventDispatcherSubscriberUtil.getSubscribedEvent(project, eventName)) {
PhpClass phpClass = PhpElementsUtil.getClass(project, event.getFqnClassName());
if(phpClass != null) {
psiElements.add(phpClass);
}
}
final ContainerCollectionResolver.ServiceCollector collector = ContainerCollectionResolver.ServiceCollector.create(project);
EventSubscriberUtil.visitNamedTag(project, "kernel.event_listener", args -> {
String event = args.getAttribute("event");
if (StringUtils.isNotBlank(event) && eventName.equals(event)) {
String serviceId = args.getServiceId();
if(StringUtils.isNotBlank(serviceId)) {
String resolve = collector.resolve(serviceId);
if(resolve != null) {
psiElements.addAll(PhpElementsUtil.getClassesInterface(project, resolve));
}
}
}
});
// loading targets on @Event
Set<String> annotationEvents = new HashSet<>();
for (VirtualFile virtualFile : FileBasedIndex.getInstance().getContainingFiles(EventAnnotationStubIndex.KEY, eventName, GlobalSearchScope.allScope(project))) {
FileBasedIndex.getInstance().processValues(EventAnnotationStubIndex.KEY, eventName, virtualFile, (virtualFile1, event) -> {
if(event.getInstance() != null && StringUtils.isNotBlank(event.getInstance())) {
annotationEvents.add(event.getInstance());
}
return true;
}, GlobalSearchScope.allScope(project));
}
// Convert class name from @Event; we need to do after collecting because of cross index access
for (String instance : annotationEvents) {
psiElements.addAll(PhpElementsUtil.getClassesInterface(project, instance));
}
return psiElements;
}
@NotNull
public static Collection<LookupElement> getEventNameLookupElements(@NotNull Project project) {
Map<String, LookupElement> results = new HashMap<>();
XmlEventParser xmlEventParser = ServiceXmlParserFactory.getInstance(project, XmlEventParser.class);
for(EventDispatcherSubscribedEvent event : xmlEventParser.getEvents()) {
results.put(event.getStringValue(), LookupElementBuilder.create(event.getStringValue()).withTypeText(event.getType(), true).withIcon(Symfony2Icons.EVENT));
}
for(EventDispatcherSubscribedEvent event: EventDispatcherSubscriberUtil.getSubscribedEvents(project)) {
results.put(event.getStringValue(), LookupElementBuilder.create(event.getStringValue()).withTypeText(event.getType(), true).withIcon(Symfony2Icons.EVENT));
}
EventSubscriberUtil.visitNamedTag(project, "kernel.event_listener", args -> {
String event = args.getAttribute("event");
if (event != null && StringUtils.isNotBlank(event)) {
results.put(event, LookupElementBuilder.create(event).withTypeText("kernel.event_listener", true).withIcon(Symfony2Icons.EVENT));
}
});
for (String s : FileIndexCaches.getIndexKeysCache(project, EVENT_ANNOTATIONS, EventAnnotationStubIndex.KEY)) {
String typeText = "Event";
// Find class name on fast index
DispatcherEvent event = ContainerUtil.find(FileBasedIndex.getInstance().getValues(
EventAnnotationStubIndex.KEY, s, GlobalSearchScope.allScope(project)),
dispatcherEvent -> dispatcherEvent.getInstance() != null
);
if(event != null) {
typeText = event.getInstance();
}
results.put(s, LookupElementBuilder.create(s).withTypeText(typeText, true).withIcon(Symfony2Icons.EVENT));
}
return results.values();
}
/**
* XML: <tag event=""fooBar/>
* YML: - event: 'foobar'
* PHP: ['foo' => 'method']
*/
@Nullable
public static String getEventNameFromScope(@NotNull PsiElement psiElement) {
// xml service
if(psiElement.getContainingFile() instanceof XmlFile) {
XmlTag xmlTag = PsiTreeUtil.getParentOfType(psiElement, XmlTag.class);
if(xmlTag == null) {
return null;
}
XmlAttribute event = xmlTag.getAttribute("event");
if(event == null) {
return null;
}
String value = event.getValue();
if(StringUtils.isBlank(value)) {
return null;
}
return value;
} else if(psiElement.getContainingFile() instanceof YAMLFile) {
// yaml services
YAMLMapping yamlHash = PsiTreeUtil.getParentOfType(psiElement, YAMLMapping.class);
if(yamlHash != null) {
YAMLKeyValue event = YamlHelper.getYamlKeyValue(yamlHash, "event");
if(event != null) {
PsiElement value = event.getValue();
if(value != null ) {
String text = value.getText();
if(StringUtils.isNotBlank(text)) {
return text;
}
}
}
}
} else if(psiElement.getContainingFile() instanceof PhpFile) {
ArrayHashElement arrayHashElement = PsiTreeUtil.getParentOfType(psiElement, ArrayHashElement.class);
if(arrayHashElement != null) {
PhpPsiElement key = arrayHashElement.getKey();
if (key != null) {
String stringValue = PhpElementsUtil.getStringValue(key);
if(StringUtils.isNotBlank(stringValue)) {
return stringValue;
}
}
}
}
return null;
}
}