package fr.adrienbrault.idea.symfony2plugin.util.dict;
import com.intellij.codeInsight.daemon.LineMarkerInfo;
import com.intellij.codeInsight.daemon.RelatedItemLineMarkerInfo;
import com.intellij.codeInsight.hint.HintManager;
import com.intellij.codeInsight.navigation.NavigationGutterIconBuilder;
import com.intellij.ide.highlighter.XmlFileType;
import com.intellij.openapi.command.WriteCommandAction;
import com.intellij.openapi.editor.Editor;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.ui.popup.JBPopupFactory;
import com.intellij.openapi.util.NotNullLazyValue;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiFile;
import com.intellij.psi.PsiManager;
import com.intellij.psi.search.GlobalSearchScope;
import com.intellij.ui.components.JBList;
import com.intellij.util.Consumer;
import com.intellij.util.containers.ContainerUtil;
import com.intellij.util.indexing.FileBasedIndex;
import com.intellij.util.indexing.FileBasedIndexImpl;
import com.jetbrains.php.PhpIcons;
import com.jetbrains.php.lang.psi.elements.Method;
import com.jetbrains.php.lang.psi.elements.Parameter;
import com.jetbrains.php.lang.psi.elements.PhpClass;
import fr.adrienbrault.idea.symfony2plugin.action.ServiceActionUtil;
import fr.adrienbrault.idea.symfony2plugin.action.generator.naming.DefaultServiceNameStrategy;
import fr.adrienbrault.idea.symfony2plugin.action.generator.naming.JavascriptServiceNameStrategy;
import fr.adrienbrault.idea.symfony2plugin.action.generator.naming.ServiceNameStrategyInterface;
import fr.adrienbrault.idea.symfony2plugin.action.generator.naming.ServiceNameStrategyParameter;
import fr.adrienbrault.idea.symfony2plugin.dic.ContainerService;
import fr.adrienbrault.idea.symfony2plugin.dic.XmlTagParser;
import fr.adrienbrault.idea.symfony2plugin.form.util.FormUtil;
import fr.adrienbrault.idea.symfony2plugin.stubs.ContainerCollectionResolver;
import fr.adrienbrault.idea.symfony2plugin.stubs.ServiceIndexUtil;
import fr.adrienbrault.idea.symfony2plugin.stubs.SymfonyProcessors;
import fr.adrienbrault.idea.symfony2plugin.stubs.indexes.ContainerParameterStubIndex;
import fr.adrienbrault.idea.symfony2plugin.stubs.indexes.ServicesTagStubIndex;
import fr.adrienbrault.idea.symfony2plugin.util.PhpElementsUtil;
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.YAMLFileType;
import java.util.*;
/**
* @author Daniel Espendiller <daniel@espendiller.net>
*/
public class ServiceUtil {
private static ServiceNameStrategyInterface[] NAME_STRATEGIES = new ServiceNameStrategyInterface[] {
new JavascriptServiceNameStrategy(),
new DefaultServiceNameStrategy(),
};
public static final Map<String , String> TAG_INTERFACES = new HashMap<String , String>() {{
put("assetic.asset", "\\Assetic\\Filter\\FilterInterface");
put("assetic.factory_worker", "\\Assetic\\Factory\\Worker\\WorkerInterface");
put("assetic.filter", "\\Assetic\\Filter\\FilterInterface");
put("assetic.formula_loader", "\\Assetic\\Factory\\Loader\\FormulaLoaderInterface");
put("assetic.formula_resource", null);
put("assetic.templating.php", null);
put("assetic.templating.twig", null);
put("console.command", "\\Symfony\\Component\\Console\\Command\\Command");
put("data_collector", "\\Symfony\\Component\\HttpKernel\\DataCollector\\DataCollectorInterface");
put("doctrine.event_listener", null);
put("doctrine.event_subscriber", null);
put("form.type", "\\Symfony\\Component\\Form\\FormTypeInterface");
put("form.type_extension", "\\Symfony\\Component\\Form\\FormTypeExtensionInterface");
put("form.type_guesser", "\\Symfony\\Component\\Form\\FormTypeGuesserInterface");
put("kernel.cache_clearer", null);
put("kernel.cache_warmer", "\\Symfony\\Component\\HttpKernel\\CacheWarmer\\CacheWarmerInterface");
put("kernel.event_subscriber", "\\Symfony\\Component\\EventDispatcher\\EventSubscriberInterface");
put("kernel.fragment_renderer", "\\Symfony\\Component\\HttpKernel\\Fragment\\FragmentRendererInterface");
put("monolog.logger", null);
put("monolog.processor", null);
put("routing.loader", "\\Symfony\\Component\\Config\\Loader\\LoaderInterface");
//put("security.remember_me_aware", null);
put("security.voter", "\\Symfony\\Component\\Security\\Core\\Authorization\\Voter\\VoterInterface");
put("serializer.encoder", "\\Symfony\\Component\\Serializer\\Encoder\\EncoderInterface");
put("serializer.normalizer", "\\Symfony\\Component\\Serializer\\Normalizer\\NormalizerInterface");
// Symfony\Component\Serializer\Normalizer\DenormalizerInterface
put("swiftmailer.default.plugin", "\\Swift_Events_EventListener");
put("templating.helper", "\\Symfony\\Component\\Templating\\Helper\\HelperInterface");
put("translation.loader", "\\Symfony\\Component\\Translation\\Loader\\LoaderInterface");
put("translation.extractor", "\\Symfony\\Component\\Translation\\Extractor\\ExtractorInterface");
put("translation.dumper", "\\Symfony\\Component\\Translation\\Dumper\\DumperInterface");
put("twig.extension", "\\Twig_Extension");
put("twig.loader", "\\Twig_LoaderInterface");
put("validator.constraint_validator", "Symfony\\Component\\Validator\\ConstraintValidator");
put("validator.initializer", "Symfony\\Component\\Validator\\ObjectInitializerInterface");
// 2.6 - @TODO: how to handle duplicate interfaces; also make them weaker
put("routing.expression_language_provider", "\\Symfony\\Component\\ExpressionLanguage\\ExpressionFunctionProviderInterface");
put("security.expression_language_provider", "\\Symfony\\Component\\ExpressionLanguage\\ExpressionFunctionProviderInterface");
}};
/**
* static event parameter list
*
* TODO: replace with live fetch; now redundant because of @Event const in Symfony itself
*/
public static final Map<String , String> TAGS = new HashMap<String , String>() {{
put("kernel.request", "\\Symfony\\Component\\HttpKernel\\Event\\GetResponseEvent");
put("kernel.view", "\\Symfony\\Component\\HttpKernel\\Event\\GetResponseForControllerResultEvent");
put("kernel.controller", "\\Symfony\\Component\\HttpKernel\\Event\\FilterControllerEvent");
put("kernel.response", "\\Symfony\\Component\\HttpKernel\\Event\\FilterResponseEvent");
put("kernel.finish_request", "\\Symfony\\Component\\HttpKernel\\Event\\FinishRequestEvent");
put("kernel.terminate", "\\Symfony\\Component\\HttpKernel\\Event\\PostResponseEvent");
put("kernel.exception", "\\Symfony\\Component\\HttpKernel\\Event\\GetResponseForExceptionEvent");
put("console.command", "\\Symfony\\Component\\Console\\Event\\ConsoleCommandEvent");
put("console.terminate", "\\Symfony\\Component\\Console\\Event\\ConsoleTerminateEvent");
put("console.exception", "\\Symfony\\Component\\Console\\Event\\ConsoleExceptionEvent");
put("form.pre_bind", "\\Symfony\\Component\\Form\\FormEvent");
put("form.bind", "\\Symfony\\Component\\Form\\FormEvent");
put("form.post_bind", "\\Symfony\\Component\\Form\\FormEvent");
put("form.pre_set_data", "\\Symfony\\Component\\Form\\FormEvent");
put("form.post_set_data", "\\Symfony\\Component\\Form\\FormEvent");
}};
/**
* %test%, service, \Class\Name to PhpClass
*/
@Nullable
public static PhpClass getResolvedClassDefinition(@NotNull Project project, @NotNull String serviceClassParameterName) {
return getResolvedClassDefinition(project, serviceClassParameterName, new ContainerCollectionResolver.LazyServiceCollector(project));
}
/**
* %test%, service, \Class\Name to PhpClass
*/
@Nullable
public static PhpClass getResolvedClassDefinition(@NotNull Project project, @NotNull String serviceClassParameterName, ContainerCollectionResolver.LazyServiceCollector collector) {
// match parameter
if(serviceClassParameterName.startsWith("%") && serviceClassParameterName.endsWith("%")) {
String serviceClass = ContainerCollectionResolver.resolveParameter(collector.getParameterCollector(), serviceClassParameterName);
if(serviceClass != null) {
return PhpElementsUtil.getClassInterface(project, serviceClass);
}
return null;
}
// service names dont have namespaces
if(!serviceClassParameterName.contains("\\")) {
String serviceClass = collector.getCollector().resolve(serviceClassParameterName);
if (serviceClass != null) {
return PhpElementsUtil.getClassInterface(project, serviceClass);
}
}
// fallback to class name with and without namespaces
return PhpElementsUtil.getClassInterface(project, serviceClassParameterName);
}
/**
* Get parameter def inside xml or yaml file
*/
public static Collection<PsiElement> getParameterDefinition(Project project, String parameterName) {
if(parameterName.length() > 2 && parameterName.startsWith("%") && parameterName.endsWith("%")) {
parameterName = parameterName.substring(1, parameterName.length() - 1);
}
Collection<PsiElement> psiElements = new ArrayList<>();
Collection<VirtualFile> fileCollection = FileBasedIndex.getInstance().getContainingFiles(ContainerParameterStubIndex.KEY, parameterName, GlobalSearchScope.getScopeRestrictedByFileTypes(GlobalSearchScope.allScope(project), XmlFileType.INSTANCE, YAMLFileType.YML));
for(VirtualFile virtualFile: fileCollection) {
PsiFile psiFile = PsiManager.getInstance(project).findFile(virtualFile);
if(psiFile != null) {
psiElements.addAll(ServiceIndexUtil.findParameterDefinitions(psiFile, parameterName));
}
}
return psiElements;
}
public static Collection<PsiElement> getServiceClassTargets(@NotNull Project project, @Nullable String value) {
List<PsiElement> resolveResults = new ArrayList<>();
if(value == null || StringUtils.isBlank(value)) {
return resolveResults;
}
// resolve class or parameter class
PhpClass phpClass = ServiceUtil.getResolvedClassDefinition(project, value);
if(phpClass != null) {
resolveResults.add(phpClass);
}
// get parameter def target
if(value.startsWith("%") && value.endsWith("%")) {
resolveResults.addAll(ServiceUtil.getParameterDefinition(project, value));
}
return resolveResults;
}
/**
* Find every service tag that's implements or extends a classes/interface of give class
*/
@NotNull
public static Set<String> getPhpClassTags(@NotNull PhpClass phpClass) {
Project project = phpClass.getProject();
SymfonyProcessors.CollectProjectUniqueKeys projectUniqueKeysStrong = new SymfonyProcessors.CollectProjectUniqueKeys(project, ServicesTagStubIndex.KEY);
FileBasedIndexImpl.getInstance().processAllKeys(ServicesTagStubIndex.KEY, projectUniqueKeysStrong, project);
ContainerCollectionResolver.ServiceCollector collector = null;
Set<String> matchedTags = new HashSet<>();
Set<String> result = projectUniqueKeysStrong.getResult();
for (String serviceName : result) {
// get service where we found our tags
List<Set<String>> values = FileBasedIndexImpl.getInstance().getValues(ServicesTagStubIndex.KEY, serviceName, GlobalSearchScope.getScopeRestrictedByFileTypes(GlobalSearchScope.allScope(project), XmlFileType.INSTANCE, YAMLFileType.YML));
if(values.size() == 0) {
continue;
}
// create unique tag list
Set<String> tags = new HashSet<>();
for(Set<String> tagValue: values) {
tags.addAll(tagValue);
}
if(collector == null) {
collector = ContainerCollectionResolver.ServiceCollector.create(project);
}
PhpClass serviceClass = ServiceUtil.getServiceClass(project, serviceName, collector);
if(serviceClass == null) {
continue;
}
boolean matched = false;
// get classes this service implements or extends
for (PhpClass serviceClassImpl: getSuperClasses(serviceClass)) {
// find interface or extends class which also implements
// @TODO: currently first level only, check recursive
if(!PhpElementsUtil.isEqualClassName(phpClass, serviceClassImpl) && PhpElementsUtil.isInstanceOf(phpClass, serviceClassImpl)) {
matched = true;
break;
}
}
if(matched) {
matchedTags.addAll(tags);
}
}
return matchedTags;
}
/**
* Get "extends" and implements on class level
*/
@NotNull
public static Set<PhpClass> getSuperClasses(@NotNull PhpClass serviceClass) {
Set<PhpClass> phpClasses = new HashSet<>();
PhpClass superClass = serviceClass.getSuperClass();
if(superClass != null) {
phpClasses.add(superClass);
}
phpClasses.addAll(Arrays.asList(serviceClass.getImplementedInterfaces()));
return phpClasses;
}
public static Set<String> getTaggedServices(Project project, String tagName) {
// @TODO: cache
SymfonyProcessors.CollectProjectUniqueKeys projectUniqueKeysStrong = new SymfonyProcessors.CollectProjectUniqueKeys(project, ServicesTagStubIndex.KEY);
FileBasedIndexImpl.getInstance().processAllKeys(ServicesTagStubIndex.KEY, projectUniqueKeysStrong, project);
Set<String> service = new HashSet<>();
for(String serviceName: projectUniqueKeysStrong.getResult()) {
List<Set<String>> serviceDefinitions = FileBasedIndexImpl.getInstance().getValues(ServicesTagStubIndex.KEY, serviceName, GlobalSearchScope.getScopeRestrictedByFileTypes(GlobalSearchScope.allScope(project), XmlFileType.INSTANCE, YAMLFileType.YML));
for(Set<String> strings: serviceDefinitions) {
if(strings.contains(tagName)) {
service.add(serviceName);
}
}
}
return service;
}
public static Collection<PhpClass> getTaggedClasses(@NotNull Project project, @NotNull String tagName) {
List<PhpClass> phpClasses = new ArrayList<>();
Set<String> taggedServices = getTaggedServices(project, tagName);
if(taggedServices.size() == 0) {
return phpClasses;
}
ContainerCollectionResolver.ServiceCollector collector = ContainerCollectionResolver.ServiceCollector.create(project);
for(String serviceName: taggedServices) {
String resolvedService = collector.resolve(serviceName);
if(resolvedService != null) {
PhpClass phpClass = PhpElementsUtil.getClass(project, resolvedService);
if(phpClass != null) {
phpClasses.add(phpClass);
}
}
}
return phpClasses;
}
public static Collection<PhpClass> getTaggedClassesWithCompiled(Project project, String tagName) {
Set<String> uniqueClass = new HashSet<>();
Collection<PhpClass> taggedClasses = new ArrayList<>();
for(PhpClass phpClass: getTaggedClasses(project, tagName)) {
String presentableFQN = phpClass.getPresentableFQN();
if(!uniqueClass.contains(presentableFQN)) {
uniqueClass.add(presentableFQN);
taggedClasses.add(phpClass);
}
}
XmlTagParser xmlTagParser = ServiceXmlParserFactory.getInstance(project, XmlTagParser.class);
List<String> taggedCompiledClasses= xmlTagParser.getTaggedClass(tagName);
if(taggedCompiledClasses == null) {
return taggedClasses;
}
for(String className: taggedCompiledClasses) {
if(!uniqueClass.contains(className)) {
PhpClass phpClass = PhpElementsUtil.getClass(project, className);
if(phpClass != null) {
taggedClasses.add(phpClass);
}
}
}
return taggedClasses;
}
/**
* Resolve "@service" to its class
*/
@Nullable
public static PhpClass getServiceClass(@NotNull Project project, @NotNull String serviceName) {
serviceName = YamlHelper.trimSpecialSyntaxServiceName(serviceName);
if(serviceName.length() == 0) {
return null;
}
ContainerService containerService = ContainerCollectionResolver.getService(project, serviceName);
if(containerService == null) {
return null;
}
String serviceClass = containerService.getClassName();
if(serviceClass == null) {
return null;
}
return PhpElementsUtil.getClassInterface(project, serviceClass);
}
/**
* Resolve "@service" to its class + proxy ServiceCollector for iteration
*/
@Nullable
public static PhpClass getServiceClass(@NotNull Project project, @NotNull String serviceName, @NotNull ContainerCollectionResolver.ServiceCollector collector) {
serviceName = YamlHelper.trimSpecialSyntaxServiceName(serviceName);
if(serviceName.length() == 0) {
return null;
}
String resolve = collector.resolve(serviceName);
if(resolve == null) {
return null;
}
return PhpElementsUtil.getClassInterface(project, resolve);
}
/**
* Gets all tags on extends/implements path of class
*/
@NotNull
public static Set<String> getPhpClassServiceTags(@NotNull PhpClass phpClass) {
Set<String> tags = new HashSet<>();
for (Map.Entry<String, String> entry : TAG_INTERFACES.entrySet()) {
if(entry.getValue() == null) {
continue;
}
if(PhpElementsUtil.isInstanceOf(phpClass, entry.getValue())) {
tags.add(entry.getKey());
}
}
// strong tags wins
if(tags.size() > 0) {
return tags;
}
// try to resolve on indexed tags
return getPhpClassTags(phpClass);
}
/**
* Event based decoration of class
*/
public static void decorateServiceTag(@NotNull ServiceTag service) {
// @TODO: provide extension
// form alias
if(service.getTagName().equals("form.type") && PhpElementsUtil.isInstanceOf(service.getPhpClass(), FormUtil.ABSTRACT_FORM_INTERFACE)) {
Collection<String> aliases = FormUtil.getFormAliases(service.getPhpClass());
if(aliases.size() > 0) {
service.addAttribute("alias", aliases.iterator().next());
}
}
}
@NotNull
public static Collection<ContainerService> getServiceSuggestionForPhpClass(@NotNull PhpClass phpClass, @NotNull Map<String, ContainerService> serviceMap) {
return getServiceSuggestionForPhpClass(phpClass, serviceMap.values());
}
@NotNull
public static Collection<ContainerService> getServiceSuggestionForPhpClass(@NotNull PhpClass phpClass, @NotNull Collection<ContainerService> serviceMap) {
String fqn = StringUtils.stripStart(phpClass.getFQN(), "\\");
Collection<ContainerService> instances = new ArrayList<>();
for(ContainerService service: serviceMap) {
if(service.getClassName() == null) {
continue;
}
PhpClass serviceClass = PhpElementsUtil.getClassInterface(phpClass.getProject(), service.getClassName());
if(serviceClass == null) {
continue;
}
if(PhpElementsUtil.isInstanceOf(serviceClass, fqn)) {
instances.add(service);
}
}
return instances;
}
@NotNull
public static Set<String> getServiceSuggestionsForServiceConstructorIndex(@NotNull Project project, @NotNull String serviceName, int index) {
PhpClass phpClass = ServiceUtil.getResolvedClassDefinition(project, serviceName);
// check type hint on constructor
if(phpClass == null) {
return Collections.emptySet();
}
Method constructor = phpClass.getConstructor();
if(constructor == null) {
return Collections.emptySet();
}
Parameter[] constructorParameter = constructor.getParameters();
if(index >= constructorParameter.length) {
return Collections.emptySet();
}
String className = constructorParameter[index].getDeclaredType().toString();
PhpClass expectedClass = PhpElementsUtil.getClassInterface(project, className);
if(expectedClass == null) {
return Collections.emptySet();
}
return ServiceActionUtil.getPossibleServices(expectedClass, ContainerCollectionResolver.getServices(project));
}
public static void insertTagWithPopupDecision(final @NotNull Editor editor, final @NotNull Set<String> phpServiceTags, final @NotNull Consumer<String> consumer) {
final JBList list = new JBList(phpServiceTags);
if(phpServiceTags.size() == 0) {
HintManager.getInstance().showErrorHint(editor, "Ops, no tag found");
return;
}
if(phpServiceTags.size() == 1) {
new WriteCommandAction.Simple(editor.getProject(), "Service Suggestion Insert") {
@Override
protected void run() {
consumer.consume(phpServiceTags.iterator().next());
}
}.execute();
return;
}
JBPopupFactory.getInstance().createListPopupBuilder(list)
.setTitle("Symfony: Tag Suggestion")
.setItemChoosenCallback(() -> new WriteCommandAction.Simple(editor.getProject(), "Service Suggestion Insert") {
@Override
protected void run() {
consumer.consume((String) list.getSelectedValue());
}
}.execute())
.createPopup()
.showInBestPositionFor(editor);
}
@NotNull
public static Collection<ContainerService> getServiceSuggestionsForTypeHint(@NotNull Method method, int index, @NotNull Collection<ContainerService> services) {
PhpClass phpClass = PhpElementsUtil.getMethodTypeHintParameterPhpClass(method, index);
if(phpClass == null) {
return Collections.emptyList();
}
return ServiceUtil.getServiceSuggestionForPhpClass(phpClass, services);
}
@NotNull
public static String getServiceNameForClass(@NotNull Project project, @NotNull String className) {
className = StringUtils.stripStart(className, "\\");
ServiceNameStrategyParameter parameter = new ServiceNameStrategyParameter(project, className);
for (ServiceNameStrategyInterface nameStrategy : NAME_STRATEGIES) {
String serviceName = nameStrategy.getServiceName(parameter);
if(serviceName != null && StringUtils.isNotBlank(serviceName)) {
return serviceName;
}
}
return className.toLowerCase().replace("\\", "_");
}
@Nullable
public static NavigationGutterIconBuilder<PsiElement> getLineMarkerForDecoratedServiceId(@NotNull Project project, @NotNull Map<String, Collection<ContainerService>> decorated, @NotNull String id) {
if(!decorated.containsKey(id)) {
return null;
}
NotNullLazyValue<Collection<? extends PsiElement>> lazy = ServiceIndexUtil.getServiceIdDefinitionLazyValue(
project,
ContainerUtil.map(decorated.get(id), ContainerService::getName)
);
return NavigationGutterIconBuilder.create(PhpIcons.IMPLEMENTS)
.setTargets(lazy)
.setTooltipText("Navigate to decoration");
}
/**
* <service id="foo_bar_main" decorates="app.mailer"/>
*/
@NotNull
public static RelatedItemLineMarkerInfo<PsiElement> getLineMarkerForDecoratesServiceId(@NotNull PsiElement psiElement, @NotNull String decorates, @NotNull Collection<LineMarkerInfo> result) {
return NavigationGutterIconBuilder.create(PhpIcons.OVERRIDEN)
.setTargets(ServiceIndexUtil.getServiceIdDefinitionLazyValue(psiElement.getProject(), Collections.singletonList(decorates)))
.setTooltipText("Navigate to decorated service")
.createLineMarkerInfo(psiElement);
}
}