package fr.adrienbrault.idea.symfony2plugin.util.yaml; import com.intellij.codeInspection.ProblemHighlightType; import com.intellij.codeInspection.ProblemsHolder; import com.intellij.openapi.application.Result; import com.intellij.openapi.command.WriteCommandAction; import com.intellij.openapi.util.Pair; import com.intellij.openapi.util.text.StringUtil; import com.intellij.patterns.PlatformPatterns; import com.intellij.psi.PsiElement; import com.intellij.psi.PsiFile; import com.intellij.psi.util.PsiTreeUtil; import com.intellij.util.Consumer; import com.intellij.util.ObjectUtils; import com.intellij.util.Processor; import com.intellij.util.containers.ContainerUtil; import com.jetbrains.php.lang.psi.elements.Parameter; import com.jetbrains.php.refactoring.PhpNameUtil; import fr.adrienbrault.idea.symfony2plugin.dic.ParameterResolverConsumer; import fr.adrienbrault.idea.symfony2plugin.dic.tags.yaml.StaticAttributeResolver; import fr.adrienbrault.idea.symfony2plugin.util.PsiElementUtils; import fr.adrienbrault.idea.symfony2plugin.util.yaml.visitor.ParameterVisitor; import fr.adrienbrault.idea.symfony2plugin.util.yaml.visitor.YamlServiceTag; import fr.adrienbrault.idea.symfony2plugin.util.yaml.visitor.YamlTagVisitor; import org.apache.commons.lang.ArrayUtils; import org.apache.commons.lang.StringUtils; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.jetbrains.yaml.YAMLElementGenerator; import org.jetbrains.yaml.YAMLUtil; import org.jetbrains.yaml.psi.*; import org.jetbrains.yaml.psi.impl.YAMLHashImpl; import java.util.*; /** * @author Daniel Espendiller <daniel@espendiller.net> */ public class YamlHelper { @Nullable static public PsiElement getLocalServiceName(PsiFile psiFile, String findServiceName) { return new YamlLocalServiceMap().getLocalServiceName(psiFile, findServiceName); } static public Map<String, String> getLocalParameterMap(PsiFile psiElement) { return new YamlLocalServiceMap().getLocalParameterMap(psiElement); } static public PsiElement getLocalParameterMap(PsiFile psiFile, String parameterName) { return new YamlLocalServiceMap().getLocalParameterName(psiFile, parameterName); } /** * getChildren eg on YamlArray is empty, provide workaround */ static public PsiElement[] getChildrenFix(PsiElement psiElement) { List<PsiElement> psiElements = new ArrayList<>(); PsiElement startElement = psiElement.getFirstChild(); if(startElement == null) { return psiElements.toArray(new PsiElement[psiElements.size()]); } psiElements.add(startElement); for (PsiElement child = psiElement.getFirstChild().getNextSibling(); child != null; child = child.getNextSibling()) { psiElements.add(child); } return psiElements.toArray(new PsiElement[psiElements.size()]); } /** * Try to find psi value which match should be a array value and filter out comma, whitespace... * [@service, "@service2", [""], ['']]; * * TODO: drop this hack; included in core now */ @NotNull static public List<YAMLSequenceItem> getYamlArrayValues(@NotNull YAMLSequence yamlArray) { return yamlArray.getItems(); } /** * FOO: * - foobar * * FOO: [foobar] */ @NotNull static public Collection<YAMLSequenceItem> getSequenceItems(@NotNull YAMLKeyValue yamlKeyValue) { PsiElement yamlSequence = yamlKeyValue.getLastChild(); if(yamlSequence instanceof YAMLSequence) { return ((YAMLSequence) yamlSequence).getItems(); } return Collections.emptyList(); } /** * [ROLE_USER, FEATURE_ALPHA, ROLE_ALLOWED_TO_SWITCH] */ @NotNull static public Collection<String> getYamlArrayValuesAsString(@NotNull YAMLSequence yamlArray) { return new HashSet<>(getYamlArrayValuesAsList(yamlArray)); } /** * [ROLE_USER, FEATURE_ALPHA, ROLE_ALLOWED_TO_SWITCH] */ @NotNull static public Collection<String> getYamlArrayValuesAsList(@NotNull YAMLSequence yamlArray) { Collection<String> keys = new ArrayList<>(); for (YAMLSequenceItem yamlSequenceItem : yamlArray.getItems()) { YAMLValue value = yamlSequenceItem.getValue(); if(!(value instanceof YAMLScalar)) { continue; } String textValue = ((YAMLScalar) value).getTextValue(); if(StringUtils.isNotBlank(textValue)) { keys.add(textValue); } } return keys; } @Nullable public static YAMLKeyValue getYamlKeyValue(@Nullable PsiElement yamlCompoundValue, String keyName) { return getYamlKeyValue(yamlCompoundValue, keyName, false); } @Nullable public static YAMLKeyValue getYamlKeyValue(@Nullable PsiElement yamlCompoundValue, String keyName, boolean ignoreCase) { if (!(yamlCompoundValue instanceof YAMLMapping)) { return null; } if (!ignoreCase) { return ((YAMLMapping) yamlCompoundValue).getKeyValueByKey(keyName); } YAMLKeyValue classKeyValue; classKeyValue = PsiElementUtils.getChildrenOfType(yamlCompoundValue, PlatformPatterns.psiElement(YAMLKeyValue.class).withName(PlatformPatterns.string().oneOfIgnoreCase(keyName))); if(classKeyValue == null) { return null; } return classKeyValue; } private static class YamlLocalServiceMap { public Map<String, String> getLocalParameterMap(PsiFile psiFile) { Map<String, String> map = new HashMap<>(); for(YAMLKeyValue yamlParameterArray: getQualifiedKeyValuesInFile((YAMLFile) psiFile, "parameters")) { String keyName = yamlParameterArray.getKeyText(); if(StringUtils.isBlank(keyName)) { continue; } // extract parameter value String textValue = null; PsiElement value = yamlParameterArray.getValue(); if(value instanceof YAMLScalar) { String myTextValue = ((YAMLScalar) value).getTextValue(); if(myTextValue.length() > 0 && myTextValue.length() < 150) { textValue = myTextValue; } } map.put(keyName.toLowerCase(), textValue); } return map; } @Nullable public PsiElement getLocalServiceName(PsiFile psiFile, String findServiceName) { return getYamlKeyPath(psiFile, findServiceName, "services"); } @Nullable public PsiElement getLocalParameterName(PsiFile psiFile, String findServiceName) { return getYamlKeyPath(psiFile, findServiceName, "parameters"); } @Nullable private PsiElement getYamlKeyPath(@NotNull PsiFile psiFile, final @NotNull String findServiceName, @NotNull String rootKey) { final Collection<PsiElement> psiElements = new ArrayList<>(); // @TODO: support case insensitive visitQualifiedKeyValuesInFile((YAMLFile) psiFile, rootKey, yamlKeyValue -> { if(findServiceName.equalsIgnoreCase(yamlKeyValue.getKeyText())) { psiElements.add(yamlKeyValue); } }); if(psiElements.size() == 0) { return null; } // @TODO: provide support for multiple targets return psiElements.iterator().next(); } } public static boolean isValidParameterName(@NotNull String parameterName) { if(parameterName.length() < 3) { return false; } if(!parameterName.startsWith("%") || !parameterName.endsWith("%") || parameterName.toLowerCase().startsWith("%env(")) { return false; } // use regular expr here? // %kernel.root_dir%/../web/%webpath_modelmasks% if(parameterName.contains("/") || parameterName.contains("..")) { return false; } // more than 2x "%" is invalid return !parameterName.substring(1, parameterName.length() - 1).contains("%"); } @NotNull public static String trimSpecialSyntaxServiceName(@NotNull String serviceName) { if(serviceName.startsWith("@")) { serviceName = serviceName.substring(1); } // yaml strict syntax if(serviceName.endsWith("=")) { serviceName = serviceName.substring(0, serviceName.length() -1); } // optional syntax if(serviceName.startsWith("?")) { serviceName = serviceName.substring(1, serviceName.length()); } return serviceName; } @Nullable public static String getYamlKeyName(@NotNull YAMLKeyValue yamlKeyValue) { PsiElement modelName = yamlKeyValue.getKey(); if(modelName == null) { return null; } String keyName = StringUtils.trim(modelName.getText()); if(keyName.endsWith(":")) { keyName = StringUtils.trim((keyName.substring(0, keyName.length() - 1))); } return keyName; } @Nullable public static Set<String> getKeySet(@Nullable YAMLKeyValue yamlKeyValue) { if(yamlKeyValue == null) { return null; } PsiElement yamlCompoundValue = yamlKeyValue.getValue(); if(yamlCompoundValue == null) { return null; } Set<String> keySet = new HashSet<>(); for(YAMLKeyValue yamlKey: PsiTreeUtil.getChildrenOfTypeAsList(yamlCompoundValue, YAMLKeyValue.class)) { String fieldName = getYamlKeyName(yamlKey); if(fieldName != null) { keySet.add(fieldName); } } return keySet; } @Nullable public static YAMLKeyValue getYamlKeyValue(@NotNull YAMLMapping yamlHash, String keyName) { return getYamlKeyValue(yamlHash, keyName, false); } @Nullable public static String getYamlKeyValueAsString(@NotNull YAMLMapping yamlHash, @NotNull String keyName) { YAMLKeyValue yamlKeyValue = getYamlKeyValue(yamlHash, keyName, false); if(yamlKeyValue == null) { return null; } final String valueText = yamlKeyValue.getValueText(); if(StringUtils.isBlank(valueText)) { return null; } return valueText; } @Nullable public static YAMLKeyValue getYamlKeyValue(@Nullable YAMLKeyValue yamlKeyValue, @NotNull String keyName) { return getYamlKeyValue(yamlKeyValue, keyName, false); } /** * foo: * class: "name" */ @Nullable public static String getYamlKeyValueAsString(@NotNull YAMLKeyValue yamlKeyValue, @NotNull String keyName) { return getYamlKeyValueAsString(yamlKeyValue, keyName, false); } @Nullable public static String getYamlKeyValueAsString(@NotNull YAMLKeyValue yamlKeyValue, @NotNull String keyName, boolean ignoreCase) { PsiElement yamlCompoundValue = yamlKeyValue.getValue(); if(!(yamlCompoundValue instanceof YAMLCompoundValue)) { return null; } return getYamlKeyValueAsString((YAMLCompoundValue) yamlCompoundValue, keyName, ignoreCase); } @Nullable public static String getYamlKeyValueAsString(@Nullable YAMLCompoundValue yamlCompoundValue, String keyName, boolean ignoreCase) { YAMLKeyValue yamlKeyValue1 = getYamlKeyValue(yamlCompoundValue, keyName, ignoreCase); if(yamlKeyValue1 == null) { return null; } String valueText = yamlKeyValue1.getValueText(); if (StringUtils.isBlank(valueText)) { return null; } return valueText; } @Nullable public static YAMLKeyValue getYamlKeyValue(@Nullable YAMLKeyValue yamlKeyValue, String keyName, boolean ignoreCase) { if(yamlKeyValue == null) { return null; } PsiElement yamlCompoundValue = yamlKeyValue.getValue(); if(!(yamlCompoundValue instanceof YAMLCompoundValue)) { return null; } return getYamlKeyValue(yamlCompoundValue, keyName, ignoreCase); } /** * foo: * bar: * | * * Will return [foo, bar] * * todo: YAMLUtil.getFullKey is useless because its not possible to prefix self item value and needs array value * @param psiElement any PsiElement inside a key value */ public static List<String> getParentArrayKeys(PsiElement psiElement) { List<String> keys = new ArrayList<>(); YAMLKeyValue yamlKeyValue = PsiTreeUtil.getParentOfType(psiElement, YAMLKeyValue.class); if(yamlKeyValue != null) { getParentArrayKeys(yamlKeyValue, keys); } return keys; } /** * Attach all parent array keys to list (foo:\n bar:): [foo, bar] * * @param yamlKeyValue current key value context * @param key the key list */ public static void getParentArrayKeys(YAMLKeyValue yamlKeyValue, List<String> key) { key.add(yamlKeyValue.getKeyText()); PsiElement yamlCompount = yamlKeyValue.getParent(); if(yamlCompount instanceof YAMLCompoundValue) { PsiElement yamlKeyValueParent = yamlCompount.getParent(); if(yamlKeyValueParent instanceof YAMLKeyValue) { getParentArrayKeys((YAMLKeyValue) yamlKeyValueParent, key); } } } /** * Migrate to processKeysAfterRoot @TODO * * @param keyContext Should be Document or YAMLCompoundValueImpl which holds the key value children */ public static void attachDuplicateKeyInspection(PsiElement keyContext, @NotNull ProblemsHolder holder) { Map<String, PsiElement> psiElementMap = new HashMap<>(); Set<PsiElement> yamlKeyValues = new HashSet<>(); Collection<YAMLKeyValue> collection = PsiTreeUtil.getChildrenOfTypeAsList(keyContext, YAMLKeyValue.class); for(YAMLKeyValue yamlKeyValue: collection) { String keyText = PsiElementUtils.trimQuote(yamlKeyValue.getKeyText()); if(StringUtils.isNotBlank(keyText)) { if(psiElementMap.containsKey(keyText)) { yamlKeyValues.add(psiElementMap.get(keyText)); yamlKeyValues.add(yamlKeyValue); } else { psiElementMap.put(keyText, yamlKeyValue); } } } if(yamlKeyValues.size() > 0) { for(PsiElement psiElement: yamlKeyValues) { if(psiElement instanceof YAMLKeyValue) { final PsiElement keyElement = ((YAMLKeyValue) psiElement).getKey(); assert keyElement != null; holder.registerProblem(keyElement, "Duplicate key", ProblemHighlightType.GENERIC_ERROR_OR_WARNING); } } } } /** * Process yaml key in second level filtered by a root: * File > roots -> "Item" * TODO: visitQualifiedKeyValuesInFile */ public static void processKeysAfterRoot(@NotNull PsiFile psiFile, @NotNull Processor<YAMLKeyValue> yamlKeyValueProcessor, @NotNull String... roots) { for (String root : roots) { YAMLKeyValue yamlKeyValue = YAMLUtil.getQualifiedKeyInFile((YAMLFile) psiFile, root); if(yamlKeyValue != null) { YAMLCompoundValue yaml = PsiTreeUtil.findChildOfType(yamlKeyValue, YAMLCompoundValue.class); if(yaml != null) { for(YAMLKeyValue yamlKeyValueVisit: PsiTreeUtil.getChildrenOfTypeAsList(yaml, YAMLKeyValue.class)) { yamlKeyValueProcessor.process(yamlKeyValueVisit); } } } } } public static boolean isRoutingFile(PsiFile psiFile) { return psiFile.getName().contains("routing") || psiFile.getVirtualFile().getPath().contains("/routing"); } /** * foo.service.method: * class: "ClassName\Foo" * arguments: * - "@twig" * - '@twig' * tags: * - { name: routing.loader, method: "crossHint<cursor>" } * */ @Nullable public static String getServiceDefinitionClass(PsiElement psiElement) { YAMLHashImpl yamlCompoundValue = PsiTreeUtil.getParentOfType(psiElement, YAMLHashImpl.class); if(yamlCompoundValue == null) { return null; } YAMLMapping yamlMapping = PsiTreeUtil.getParentOfType(yamlCompoundValue, YAMLMapping.class); if(yamlMapping == null) { return null; } YAMLKeyValue aClass = yamlMapping.getKeyValueByKey("class"); if(aClass == null) { return null; } return aClass.getValueText(); } /** * Simplify getting of array psi elements in array or sequence context * * arguments: [@foo] * arguments: * - @foo * * TODO: can be handled nice know because on new yaml plugin */ @Nullable public static List<PsiElement> getYamlArrayOnSequenceOrArrayElements(@NotNull YAMLCompoundValue yamlCompoundValue) { if (yamlCompoundValue instanceof YAMLSequence) { return new ArrayList<>(((YAMLSequence) yamlCompoundValue).getItems()); } if (yamlCompoundValue instanceof YAMLMapping) { return new ArrayList<>(((YAMLMapping) yamlCompoundValue).getKeyValues()); } return null; } /** * Finds top most service of any given PsiElement context * * @param psiElement any PsiElement that is inside an service definition */ @Nullable public static YAMLKeyValue findServiceInContext(@NotNull PsiElement psiElement) { YAMLKeyValue serviceSubKey = PsiTreeUtil.getParentOfType(psiElement, YAMLKeyValue.class); if(serviceSubKey == null) { return null; } PsiElement serviceSubKeyCompound = serviceSubKey.getParent(); // we are inside a YAMLHash element, find most parent array key // { name: foo } if(serviceSubKeyCompound instanceof YAMLHashImpl) { YAMLKeyValue yamlKeyValue = PsiTreeUtil.getParentOfType(serviceSubKeyCompound, YAMLKeyValue.class); if(yamlKeyValue == null) { return null; } serviceSubKeyCompound = yamlKeyValue.getParent(); } // find array key inside service and check if we are inside "services" if(serviceSubKeyCompound instanceof YAMLCompoundValue) { PsiElement serviceKey = serviceSubKeyCompound.getParent(); if(serviceKey instanceof YAMLKeyValue) { PsiElement servicesKeyCompound = serviceKey.getParent(); if(servicesKeyCompound instanceof YAMLCompoundValue) { PsiElement servicesKey = servicesKeyCompound.getParent(); if(servicesKey instanceof YAMLKeyValue) { if("services".equals(((YAMLKeyValue) servicesKey).getKeyText())) { return (YAMLKeyValue) serviceKey; } } } } } return null; } /** * Collect defined service tags on a sequence list * - { name: assetic.factory_worker } * - [ assetic.factory_worker ] * * @param yamlKeyValue the service key value to find the "tags" key on * @return tag names */ @Nullable public static Set<String> collectServiceTags(@NotNull YAMLKeyValue yamlKeyValue) { YAMLKeyValue tagsKeyValue = YamlHelper.getYamlKeyValue(yamlKeyValue, "tags"); if(tagsKeyValue == null) { return null; } PsiElement tagsCompound = tagsKeyValue.getValue(); if(!(tagsCompound instanceof YAMLSequence)) { return null; } Set<String> tags = new HashSet<>(); for (YAMLSequenceItem yamlSequenceItem : ((YAMLSequence) tagsCompound).getItems()) { YAMLValue value = yamlSequenceItem.getValue(); if(value instanceof YAMLMapping) { // tags: // - {name: foobar} String name = YamlHelper.getYamlKeyValueAsString(((YAMLMapping) value), "name"); if(name != null) { tags.add(name); } } else if(value instanceof YAMLScalar) { // tags: [foobar] String textValue = ((YAMLScalar) value).getTextValue(); if(StringUtils.isNotBlank(textValue)) { tags.add(textValue); } } } return tags; } /** * TODO: use visitor pattern for all tags, we are using them to often */ public static void visitTagsOnServiceDefinition(@NotNull YAMLKeyValue yamlServiceKeyValue, @NotNull YamlTagVisitor visitor) { YAMLKeyValue tagTag = YamlHelper.getYamlKeyValue(yamlServiceKeyValue, "tags"); if(tagTag == null) { return; } final YAMLValue tagsValue = tagTag.getValue(); if(!(tagsValue instanceof YAMLSequence)) { return; } String serviceId = yamlServiceKeyValue.getKeyText(); for(YAMLSequenceItem yamlSequenceItem: ((YAMLSequence) tagsValue).getItems()) { final YAMLValue itemValue = yamlSequenceItem.getValue(); if(itemValue instanceof YAMLMapping) { // tags: // - {name: foobar} final YAMLMapping yamlHash = (YAMLMapping) itemValue; String tagName = YamlHelper.getYamlKeyValueAsString(yamlHash, "name"); if(tagName != null) { visitor.visit(new YamlServiceTag(serviceId, tagName, yamlHash)); } } else if(itemValue instanceof YAMLScalar) { // tags: [foobar] String textValue = ((YAMLScalar) itemValue).getTextValue(); if(StringUtils.isNotBlank(textValue)) { visitor.visit(new YamlServiceTag(serviceId, textValue, new StaticAttributeResolver("name", textValue))); } } } } /** * Get all children key values of a parent key value */ @NotNull private static Collection<YAMLKeyValue> getNextKeyValues(@NotNull YAMLKeyValue yamlKeyValue) { final Collection<YAMLKeyValue> yamlKeyValues = new ArrayList<>(); visitNextKeyValues(yamlKeyValue, yamlKeyValues::add); return yamlKeyValues; } /** * Visit all children key values of a parent key value */ private static void visitNextKeyValues(@NotNull YAMLKeyValue yamlKeyValue, @NotNull Consumer<YAMLKeyValue> consumer) { List<YAMLPsiElement> yamlElements = yamlKeyValue.getYAMLElements(); // @TODO: multiple? if(yamlElements.size() != 1) { return; } YAMLPsiElement next = yamlElements.iterator().next(); if(!(next instanceof YAMLMapping)) { return; } for (YAMLKeyValue keyValue : ((YAMLMapping) next).getKeyValues()) { consumer.consume(keyValue); } } /** * Get all key values in first level key visit * * parameters: * foo: "foo" */ @NotNull public static Collection<YAMLKeyValue> getQualifiedKeyValuesInFile(@NotNull YAMLFile yamlFile, @NotNull String firstLevelKeyToVisit) { YAMLKeyValue parameters = YAMLUtil.getQualifiedKeyInFile(yamlFile, firstLevelKeyToVisit); if(parameters == null) { return Collections.emptyList(); } return getNextKeyValues(parameters); } /** * Visit all key values in first level key * * parameters: * foo: "foo" */ public static void visitQualifiedKeyValuesInFile(@NotNull YAMLFile yamlFile, @NotNull String firstLevelKeyToVisit, @NotNull Consumer<YAMLKeyValue> consumer) { YAMLKeyValue parameters = YAMLUtil.getQualifiedKeyInFile(yamlFile, firstLevelKeyToVisit); if(parameters == null) { return; } visitNextKeyValues(parameters, consumer); } @Nullable public static String getStringValueOfKeyInProbablyMapping(@Nullable YAMLValue node, @NotNull String keyText) { YAMLKeyValue mapping = YAMLUtil.findKeyInProbablyMapping(node, keyText); if(mapping == null) { return null; } YAMLValue value = mapping.getValue(); if(value == null) { return null; } return value.getText(); } @NotNull public static Collection<YAMLKeyValue> getTopLevelKeyValues(@NotNull YAMLFile yamlFile) { YAMLDocument yamlDocument = PsiTreeUtil.getChildOfType(yamlFile, YAMLDocument.class); if(yamlDocument == null) { return Collections.emptyList(); } YAMLValue topLevelValue = yamlDocument.getTopLevelValue(); if(!(topLevelValue instanceof YAMLMapping)) { return Collections.emptyList(); } return ((YAMLMapping) topLevelValue).getKeyValues(); } /** * Returns "@foo" value of ["@foo", "fo<caret>o"] */ @Nullable public static String getPreviousSequenceItemAsText(@NotNull PsiElement psiElement) { PsiElement yamlScalar = psiElement.getParent(); if(!(yamlScalar instanceof YAMLScalar)) { return null; } PsiElement yamlSequence = yamlScalar.getParent(); if(!(yamlSequence instanceof YAMLSequenceItem)) { return null; } // @TODO: catch new lexer error on empty item [<caret>,@foo] "PsiErrorElement:Sequence item expected" YAMLSequenceItem prevSequenceItem = PsiTreeUtil.getPrevSiblingOfType(yamlSequence, YAMLSequenceItem.class); if(prevSequenceItem == null) { return null; } YAMLValue value = prevSequenceItem.getValue(); if(!(value instanceof YAMLScalar)) { return null; } return ((YAMLScalar) value).getTextValue(); } private interface KeyInsertValueFormatter { @Nullable String format(@NotNull YAMLMapping yamlMapping, @NotNull String chainedKey); } /** * Adds a yaml key on path. This implemention merge values and support nested key values * foo:\n bar: car -> foo.car.foo.bar * * @param formatter any string think of provide qoute */ @Nullable public static PsiElement insertKeyIntoFile(final @NotNull YAMLFile yamlFile, @NotNull KeyInsertValueFormatter formatter, @NotNull String... keys) { final Pair<YAMLKeyValue, String[]> lastKeyStorage = findLastKnownKeyInFile(yamlFile, keys); if(lastKeyStorage.getSecond().length == 0) { return null; } YAMLMapping childOfType = null; // root condition if(lastKeyStorage.getFirst() == null && lastKeyStorage.getSecond().length == keys.length) { YAMLValue topLevelValue = yamlFile.getDocuments().get(0).getTopLevelValue(); if(topLevelValue instanceof YAMLMapping) { childOfType = (YAMLMapping) topLevelValue; } } else if(lastKeyStorage.getFirst() != null) { // found a key value in key path append it there childOfType = PsiTreeUtil.getChildOfType(lastKeyStorage.getFirst(), YAMLMapping.class); } if(childOfType == null) { return null; } // pre-generate an empty key value String chainedKey = YAMLElementGenerator.createChainedKey(Arrays.asList(lastKeyStorage.getSecond()), YAMLUtil.getIndentInThisLine(childOfType)); // append value: should be string with right indent for key value String value = formatter.format(childOfType, chainedKey); if(value != null) { chainedKey += value; } YAMLFile dummyFile = YAMLElementGenerator.getInstance(yamlFile.getProject()).createDummyYamlWithText(chainedKey); final YAMLKeyValue next = PsiTreeUtil.collectElementsOfType(dummyFile, YAMLKeyValue.class).iterator().next(); if(next == null) { return null; } // finally wirte changes final YAMLMapping finalChildOfType = childOfType; new WriteCommandAction(yamlFile.getProject()) { @Override protected void run(@NotNull Result result) throws Throwable { finalChildOfType.putKeyValue(next); } @Override public String getGroupID() { return "Key insertion"; } }.execute(); return childOfType; } @Nullable public static PsiElement insertKeyIntoFile(final @NotNull YAMLFile yamlFile, final @NotNull YAMLKeyValue yamlKeyValue, @NotNull String... keys) { String keyText = yamlKeyValue.getKeyText(); return insertKeyIntoFile(yamlFile, (yamlMapping, chainedKey) -> { String text = yamlKeyValue.getText(); final String previousIndent = StringUtil.repeatSymbol(' ', YAMLUtil.getIndentInThisLine(yamlMapping)); // split content of array value object; // drop first item as getValueText() removes our key indent String[] remove = (String[]) ArrayUtils.remove(text.split("\\r?\\n"), 0); List<String> map = ContainerUtil.map(remove, s -> previousIndent + s); return "\n" + StringUtils.strip(StringUtils.join(map, "\n"), "\n"); }, (String[]) ArrayUtils.add(keys, keyText)); } public static PsiElement insertKeyIntoFile(final @NotNull YAMLFile yamlFile, final @Nullable String value, @NotNull String... keys) { return insertKeyIntoFile(yamlFile, (yamlMapping, chainedKey) -> " " + value, keys); } /** * Find last known KeyValue of key path, so that we can merge new incoming keys */ @NotNull private static Pair<YAMLKeyValue, String[]> findLastKnownKeyInFile(@NotNull YAMLFile file, @NotNull String... keys) { YAMLKeyValue last = null; YAMLMapping mapping = ObjectUtils.tryCast(file.getDocuments().get(0).getTopLevelValue(), YAMLMapping.class); for (int i = 0; i < keys.length; i++) { String s = keys[i]; if (mapping == null) { return Pair.create(last, Arrays.copyOfRange(keys, i, keys.length)); } YAMLKeyValue keyValue = mapping.getKeyValueByKey(s); if (keyValue == null) { return Pair.create(last, Arrays.copyOfRange(keys, i, keys.length)); } last = keyValue; mapping = ObjectUtils.tryCast(keyValue.getValue(), YAMLMapping.class); } return Pair.create(last, new String[]{}); } /** * Bridge to allow YAMLKeyValue adding child key-values elements. * Yaml plugin provides key adding only on YAMLMapping * * ser<caret>vice: * foo: "aaa" * */ @Nullable public static YAMLKeyValue putKeyValue(@NotNull YAMLKeyValue yamlKeyValue, @NotNull String keyName, @NotNull String valueText) { // create "foo: foo" YAMLKeyValue newYamlKeyValue = YAMLElementGenerator.getInstance(yamlKeyValue.getProject()) .createYamlKeyValue(keyName, valueText); YAMLMapping childOfAnyType = PsiTreeUtil.findChildOfAnyType(yamlKeyValue, YAMLMapping.class); if(childOfAnyType == null) { return null; } childOfAnyType.putKeyValue(newYamlKeyValue); return newYamlKeyValue; } /** * Services id in Symfony 3.3 are allowed to be class names * defensive extract by naming strategy */ public static boolean isClassServiceId(@NotNull String serviceId) { // foo.bar if(serviceId.contains(".")) { return false; } // Foo\Bar if(serviceId.contains("\\")) { return true; } // classes without namespaces "FooBar"? return false; } /** * service_name: * class: FOOBAR * calls: * - [onF<caret>oobar, []] * * FOOBAR: * calls: * - [onF<caret>oobar, []] */ public static void visitServiceCall(@NotNull YAMLScalar yamlScalar, @NotNull Consumer<String> consumer) { PsiElement yamlSeq = yamlScalar.getContext(); if(yamlSeq instanceof YAMLSequenceItem) { PsiElement context = yamlSeq.getContext(); if(context instanceof YAMLSequence) { PsiElement yamlSequenceItem = context.getParent(); if(yamlSequenceItem instanceof YAMLSequenceItem) { PsiElement yamlSeq1 = yamlSequenceItem.getParent(); if(yamlSeq1 instanceof YAMLSequence) { PsiElement callYamlKeyValue = yamlSeq1.getParent(); if(callYamlKeyValue instanceof YAMLKeyValue) { YAMLKeyValue classKeyValue = YamlHelper.getYamlKeyValue(callYamlKeyValue.getContext(), "class"); if(classKeyValue != null) { // "class" key found use this as valid class name String valueText = classKeyValue.getValueText(); if(StringUtils.isNotBlank(valueText)) { consumer.consume(valueText); } } else { // named services; key is our class name PsiElement yamlMapping = callYamlKeyValue.getParent(); if(yamlMapping instanceof YAMLMapping) { PsiElement parent = yamlMapping.getParent(); if(parent instanceof YAMLKeyValue) { String keyText = ((YAMLKeyValue) parent).getKeyText(); if(!keyText.contains(".") && PhpNameUtil.isValidNamespaceFullName(keyText)) { consumer.consume(keyText); } } } } } } } } } } /** * service_name: * class: FOOBAR * calls: * - [onFoobar, [@fo<caret>o]] * * FOOBAR: * calls: * - [onFoobar, [@fo<caret>o]] */ public static void visitServiceCallArgument(@NotNull YAMLScalar yamlScalar, @NotNull Consumer<ParameterVisitor> consumer) { PsiElement context = yamlScalar.getContext(); if(context instanceof YAMLSequenceItem) { // [@foobar, @fo<caret>obar] YAMLSequenceItem argumentSequenceItem = (YAMLSequenceItem) context; if (argumentSequenceItem.getContext() instanceof YAMLSequence) { YAMLSequence yamlCallParameterArray = (YAMLSequence) argumentSequenceItem.getContext(); PsiElement callSequenceItem = yamlCallParameterArray.getContext(); if(callSequenceItem instanceof YAMLSequenceItem) { YAMLSequenceItem enclosingItem = (YAMLSequenceItem) callSequenceItem; if (enclosingItem.getContext() instanceof YAMLSequence) { YAMLSequence yamlCallArray = (YAMLSequence) enclosingItem.getContext(); PsiElement seqItem = yamlCallArray.getContext(); if(seqItem instanceof YAMLSequenceItem) { // - [ setFoo, [@args_bar] ] PsiElement callYamlSeq = seqItem.getContext(); if(callYamlSeq instanceof YAMLSequence) { // only given method and args are valid "setFoo, [@args_bar]" List<YAMLSequenceItem> methodParameter = YamlHelper.getYamlArrayValues(yamlCallArray); if(methodParameter.size() > 1) { YAMLValue methodNameElement = methodParameter.get(0).getValue(); if(methodNameElement instanceof YAMLScalar) { String methodName = ((YAMLScalar) methodNameElement).getTextValue(); if(StringUtils.isNotBlank(methodName)) { PsiElement callYamlKeyValue = callYamlSeq.getContext(); if(callYamlKeyValue instanceof YAMLKeyValue) { final YAMLKeyValue classKeyValue = ((YAMLKeyValue) callYamlKeyValue).getParentMapping().getKeyValueByKey("class"); if (classKeyValue != null) { String valueText = classKeyValue.getValueText(); if (StringUtils.isNotBlank(valueText)) { consumer.consume(new ParameterVisitor( valueText, methodName, PsiElementUtils.getPrevSiblingsOfType(argumentSequenceItem, PlatformPatterns.psiElement(YAMLSequenceItem.class)).size()) ); } } else { // named services; key is our class name PsiElement yamlMapping = callYamlKeyValue.getParent(); if(yamlMapping instanceof YAMLMapping) { PsiElement parent = yamlMapping.getParent(); if(parent instanceof YAMLKeyValue) { String keyText = ((YAMLKeyValue) parent).getKeyText(); if(!keyText.contains(".") && PhpNameUtil.isValidNamespaceFullName(keyText)) { consumer.consume(new ParameterVisitor( keyText, methodName, PsiElementUtils.getPrevSiblingsOfType(argumentSequenceItem, PlatformPatterns.psiElement(YAMLSequenceItem.class)).size()) ); } } } } } } } } } } } } } } } /** * Consumer for method parameter match * * service_name: * class: FOOBAR * calls: * - [onFoobar, [@fo<caret>o]] */ public static void visitServiceCallArgumentMethodIndex(@NotNull YAMLScalar yamlScalar, @NotNull Consumer<Parameter> consumer) { YamlHelper.visitServiceCallArgument(yamlScalar, new ParameterResolverConsumer(yamlScalar.getProject(), consumer)); } }