package com.intellij.javascript.flex.mxml.schema; import com.intellij.codeInsight.completion.CompletionInitializationContext; import com.intellij.codeInsight.daemon.Validator; import com.intellij.icons.AllIcons; import com.intellij.javascript.flex.FlexAnnotationNames; import com.intellij.javascript.flex.FlexMxmlLanguageAttributeNames; import com.intellij.javascript.flex.FlexPredefinedTagNames; import com.intellij.javascript.flex.FlexStateElementNames; import com.intellij.javascript.flex.mxml.FlexNameAlias; import com.intellij.javascript.flex.mxml.MxmlJSClass; import com.intellij.javascript.flex.resolve.ActionScriptClassResolver; import com.intellij.lang.LanguageNamesValidation; import com.intellij.lang.javascript.JSBundle; import com.intellij.lang.javascript.JavaScriptSupportLoader; import com.intellij.lang.javascript.flex.AnnotationBackedDescriptor; import com.intellij.lang.javascript.flex.FlexBundle; import com.intellij.lang.javascript.flex.FlexUtils; import com.intellij.lang.javascript.flex.XmlBackedJSClassImpl; import com.intellij.lang.javascript.flex.sdk.FlexSdkUtils; import com.intellij.lang.javascript.psi.*; import com.intellij.lang.javascript.psi.ecmal4.*; import com.intellij.lang.javascript.psi.resolve.*; import com.intellij.lang.refactoring.NamesValidator; import com.intellij.openapi.diagnostic.Logger; import com.intellij.openapi.util.Comparing; import com.intellij.openapi.util.Ref; import com.intellij.openapi.util.text.StringUtil; import com.intellij.psi.PsiElement; import com.intellij.psi.PsiFile; import com.intellij.psi.ResolveState; import com.intellij.psi.xml.*; import com.intellij.util.ArrayUtil; import com.intellij.util.PlatformIcons; import com.intellij.util.containers.HashSet; import com.intellij.util.text.StringTokenizer; import com.intellij.xml.*; import com.intellij.xml.impl.BasicXmlAttributeDescriptor; import com.intellij.xml.impl.schema.AnyXmlAttributeDescriptor; import com.intellij.xml.util.XmlUtil; import gnu.trove.THashSet; import org.jetbrains.annotations.NonNls; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import javax.swing.*; import java.util.HashMap; import java.util.LinkedList; import java.util.List; import java.util.Set; /** * @author Maxim.Mossienko */ public class AnnotationBackedDescriptorImpl extends BasicXmlAttributeDescriptor implements Validator<XmlElement>, AnnotationBackedDescriptor, XmlElementDescriptorAwareAboutChildren { enum OriginatingElementType {Metadata, VarOrFunction, IdAttribute, Other} private static final String[] DEFERRED_IMMEDIATE = {"deferred", "immediate"}; private static final String[] AUTO_NEVER = {"auto", "never"}; private static final String[] E4X_XML = {"e4x", "xml"}; private static final String[] TRUE_FALSE = {"true", "false"}; private static final String CLEAR_DIRECTIVE = "@Clear()"; private static final String I_STYLE_CLIENT_CLASS = "mx.styles.IStyleClient"; // lowercased values listed in flash.css.Descriptor class from Flex compiler private static final String[] COLOR_ALIASES = {"black", "blue", "green", "gray", "silver", "lime", "olive", "white", "yellow", "maroon", "magenta", "navy", "red", "purple", "teal", "fuchsia", "aqua", "cyan", "halogreen", "haloblue", "haloorange", "halosilver"}; protected final String name; protected final ClassBackedElementDescriptor parentDescriptor; protected final boolean predefined; protected String type; protected String format; private boolean myEnumerated; private boolean myEnumeratedValuesCaseSensitive = true; private String[] myEnumeratedValues; private final String arrayElementType; @NonNls private static final String ENUMERATION_ATTR_NAME = "enumeration"; @NonNls private static final String FORMAT_ATTR_NAME = "format"; @NonNls private static final String TYPE_ATTR_NAME = "type"; private String percentProxy; private boolean myScriptable; private boolean myProperty = false; private String myAnnotationName; @NotNull private final OriginatingElementType myOriginatingElementType; private boolean myRichTextContent; private boolean myCollapseWhiteSpace; private final boolean myDeferredInstance; private boolean myIsRequired = false; protected AnnotationBackedDescriptorImpl(String _name, ClassBackedElementDescriptor _parentDescriptor, boolean _predefined, String _type, String _arrayElementType, boolean deferredInstance, PsiElement originatingElement) { name = _name; parentDescriptor = _parentDescriptor; predefined = _predefined; type = _type; arrayElementType = _arrayElementType; myDeferredInstance = deferredInstance; if (originatingElement instanceof JSAttributeNameValuePair) { myOriginatingElementType = OriginatingElementType.Metadata; final JSAttribute attribute = (JSAttribute)originatingElement.getParent(); initFromAttribute(attribute); } else if (originatingElement instanceof JSAttributeListOwner) { myOriginatingElementType = OriginatingElementType.VarOrFunction; myProperty = originatingElement instanceof JSFunction || originatingElement instanceof JSVariable; JSAttribute attribute = findInspectableAttr(originatingElement); initFromAttribute(attribute); attribute = findAttr((JSAttributeListOwner)originatingElement, FlexAnnotationNames.PERCENT_PROXY); if (attribute != null) { JSAttributeNameValuePair[] values = attribute.getValues(); percentProxy = values.length > 0 ? values[0].getSimpleValue() : ""; } if (type == null) { type = ClassBackedElementDescriptor.getPropertyType((JSNamedElement)originatingElement); } initFromType(); initRichTextContentAndCollapseWhiteSpace((JSAttributeListOwner)originatingElement); } else if (originatingElement instanceof XmlAttribute && FlexMxmlLanguageAttributeNames.ID.equals(((XmlAttribute)originatingElement).getName())) { myOriginatingElementType = OriginatingElementType.IdAttribute; } else { myOriginatingElementType = OriginatingElementType.Other; // dead branch? } if (predefined) { if (FlexStateElementNames.ITEM_CREATION_POLICY.equals(name)) { myEnumerated = true; myEnumeratedValues = DEFERRED_IMMEDIATE; } else if (FlexStateElementNames.ITEM_DESTRUCTION_POLICY.equals(name)) { myEnumerated = true; myEnumeratedValues = AUTO_NEVER; } else if (CodeContext.FORMAT_ATTR_NAME.equals(name) && MxmlJSClass.XML_TAG_NAME.equals(parentDescriptor.getName())) { myEnumerated = true; myEnumeratedValues = E4X_XML; myEnumeratedValuesCaseSensitive = false; } else if (CodeContext.TWO_WAY_ATTR_NAME.equals(name) && FlexPredefinedTagNames.BINDING.equals(parentDescriptor.getName())) { myEnumerated = true; myEnumeratedValues = TRUE_FALSE; myEnumeratedValuesCaseSensitive = false; } } } protected AnnotationBackedDescriptorImpl(String name, ClassBackedElementDescriptor parentDescriptor, boolean predefined, String type, String arrayElementType, PsiElement originatingElement) { this(name, parentDescriptor, predefined, type, arrayElementType, false, originatingElement); } protected AnnotationBackedDescriptorImpl(String _name, ClassBackedElementDescriptor _parentDescriptor, AnnotationBackedDescriptorImpl originalDescriptor, JSNamedElement originatingElement) { if (originalDescriptor.myOriginatingElementType != OriginatingElementType.VarOrFunction) { Logger.getInstance(AnnotationBackedDescriptorImpl.class.getName()).error(originalDescriptor.myOriginatingElementType); } myOriginatingElementType = originalDescriptor.myOriginatingElementType; name = _name; parentDescriptor = _parentDescriptor; predefined = originalDescriptor.predefined; type = originalDescriptor.type; arrayElementType = originalDescriptor.arrayElementType; myEnumerated = originalDescriptor.myEnumerated; myEnumeratedValues = originalDescriptor.getEnumeratedValues(); myScriptable = originalDescriptor.myScriptable; percentProxy = originalDescriptor.percentProxy; type = originalDescriptor.type; format = originalDescriptor.format; myAnnotationName = originalDescriptor.myAnnotationName; myProperty = originalDescriptor.myProperty; myCollapseWhiteSpace = originalDescriptor.myCollapseWhiteSpace; myRichTextContent = originalDescriptor.myRichTextContent; myDeferredInstance = originalDescriptor.myDeferredInstance; // only function (setter/getter) may be overridden if (originatingElement instanceof JSFunction) { // mxml compiler doesn't inherit function annotations initRichTextContentAndCollapseWhiteSpace((JSFunction)originatingElement); } if (originatingElement instanceof JSAttributeListOwner) { final JSAttribute attribute = findAttr((JSAttributeListOwner)originatingElement, FlexAnnotationNames.PERCENT_PROXY); if (attribute != null && percentProxy == null) { final JSAttributeNameValuePair[] values = attribute.getValues(); percentProxy = values.length > 0 ? values[0].getSimpleValue() : ""; } } } private void initRichTextContentAndCollapseWhiteSpace(JSAttributeListOwner attributeListOwner) { if (!myEnumerated) { if (contentIsArrayable()) { myRichTextContent = findAttr(attributeListOwner, FlexAnnotationNames.RICH_TEXT_CONTENT) != null; } if (JSCommonTypeNames.STRING_CLASS_NAME.equals(type)) { myCollapseWhiteSpace = findAttr(attributeListOwner, FlexAnnotationNames.COLLAPSE_WHITE_SPACE) != null; } } } @Override @NonNls public String getName() { return name; } @Override public String getFormat() { return format; } @NotNull OriginatingElementType getOriginatingElementType() { return myOriginatingElementType; } boolean isPreferredTo(@NotNull AnnotationBackedDescriptorImpl other) { // if there's event and function/variable with the same name then event is preferred // in case of style or effect and function/variable with the same name - function/variable is preferred assert Comparing.strEqual(name, other.name); if (FlexAnnotationNames.EVENT.equals(myAnnotationName) && !FlexAnnotationNames.EVENT.equals(other.myAnnotationName)) { return true; } if (myOriginatingElementType != OriginatingElementType.Metadata && other.myOriginatingElementType == OriginatingElementType.Metadata) { return true; } return false; } @Override @Nullable public PsiElement getDeclaration() { final PsiElement[] result = new PsiElement[1]; final PsiElement parentDescriptorDeclaration = parentDescriptor.getDeclaration(); PsiElement element = parentDescriptorDeclaration; if (predefined) return element; if (myOriginatingElementType == OriginatingElementType.IdAttribute) { return findDeclarationByIdAttributeValue(parentDescriptorDeclaration, new THashSet<>()); } while (element instanceof XmlFile) { XmlTag rootTag = ((XmlFile)element).getDocument().getRootTag(); element = JSResolveUtil.getClassFromTagNameInMxml(rootTag.getFirstChild()); if (element instanceof XmlBackedJSClassImpl) { element = element.getParent().getContainingFile(); } } final JSNamedElement jsClass = (JSNamedElement)element; final ClassBackedElementDescriptor.AttributedItemsProcessor itemsProcessor = new ClassBackedElementDescriptor.AttributedItemsProcessor() { @Override public boolean process(final JSNamedElement jsNamedElement, final boolean isPackageLocalVisibility) { if (myOriginatingElementType != OriginatingElementType.Metadata && name.equals(jsNamedElement.getName())) { result[0] = jsNamedElement; return false; } return true; } @Override public boolean process(final JSAttributeNameValuePair pair, final String annotationName, final boolean included) { if (myOriginatingElementType == OriginatingElementType.Metadata && name.equals(pair.getSimpleValue()) && included) { result[0] = pair; return false; } return true; } }; boolean b = jsClass == null || ClassBackedElementDescriptor.processAttributes(jsClass, itemsProcessor); if (b && jsClass instanceof JSClass) { final JSClass clazz = (JSClass)jsClass; final JSClass[] classes = clazz.getSuperClasses(); if (classes.length > 0 && clazz.getName().equals(classes[0].getName()) && clazz != classes[0]) { b = ClassBackedElementDescriptor.processAttributes(classes[0], itemsProcessor); } } if (b && parentDescriptorDeclaration instanceof XmlFile) { final JSResolveUtil.JSInjectedFilesVisitor injectedFilesVisitor = new JSResolveUtil.JSInjectedFilesVisitor() { @Override protected void process(final JSFile file) { ClassBackedElementDescriptor.processAttributes(file, itemsProcessor); } }; element = parentDescriptorDeclaration; while (element instanceof XmlFile) { XmlTag rootTag = ((XmlFile)element).getDocument().getRootTag(); FlexUtils.processMxmlTags(rootTag, true, injectedFilesVisitor); if (result[0] != null) break; element = JSResolveUtil.getClassFromTagNameInMxml(rootTag.getFirstChild()); if (element instanceof XmlBackedJSClassImpl) { element = element.getParent().getContainingFile(); } } } return result[0]; } @Override public PsiElement getEnumeratedValueDeclaration(XmlElement xmlElement, String value) { for (String s : getEnumeratedValues()) { if (Comparing.equal(s, value, myEnumeratedValuesCaseSensitive)) { return getDeclaration(); } } return null; } @Nullable private XmlAttributeValue findDeclarationByIdAttributeValue(final PsiElement descriptorDeclaration, final Set<JSClass> visited) { final Ref<XmlAttributeValue> resultRef = new Ref<>(null); if (descriptorDeclaration instanceof XmlFile) { final XmlDocument document = ((XmlFile)descriptorDeclaration).getDocument(); final XmlTag rootTag = document == null ? null : document.getRootTag(); if (rootTag != null) { ClassBackedElementDescriptor.processClassBackedTagsWithIdAttribute(rootTag, idAttributeAndItsType -> { final XmlAttributeValue xmlAttributeValue = idAttributeAndItsType.first.getValueElement(); if (xmlAttributeValue != null && xmlAttributeValue.getValue().equals(this.name)) { resultRef.set(xmlAttributeValue); return false; } return true; }); } if (resultRef.isNull()) { final JSClass jsClass = XmlBackedJSClassFactory.getXmlBackedClass((XmlFile)descriptorDeclaration); if (jsClass != null) { return findDeclarationByIdAttributeValueInSuperClass(jsClass, visited); } } } else if (descriptorDeclaration instanceof JSClass) { return findDeclarationByIdAttributeValueInSuperClass((JSClass)descriptorDeclaration, visited); } return resultRef.get(); } @Nullable private XmlAttributeValue findDeclarationByIdAttributeValueInSuperClass(@NotNull final JSClass jsClass, final Set<JSClass> visited) { if (!visited.add(jsClass)) { return null; } for (final JSClass superClass : jsClass.getSuperClasses()) { if (superClass instanceof XmlBackedJSClassImpl) { final PsiFile psiFile = superClass.getContainingFile(); if (psiFile instanceof XmlFile) { final XmlAttributeValue result = findDeclarationByIdAttributeValue(psiFile, visited); if (result != null) { return result; } } } else { return findDeclarationByIdAttributeValueInSuperClass(superClass, visited); } } return null; } @Override public void init(final PsiElement element) { } private void initFromAttribute(final JSAttribute attribute) { if (attribute == null) return; myEnumerated = attribute.getValueByName(ENUMERATION_ATTR_NAME) != null; JSAttributeNameValuePair pair = attribute.getValueByName(TYPE_ATTR_NAME); if (pair != null) type = pair.getSimpleValue(); pair = attribute.getValueByName(FORMAT_ATTR_NAME); if (pair != null) format = pair.getSimpleValue(); myAnnotationName = attribute.getName(); myScriptable = FlexAnnotationNames.EVENT.equals(myAnnotationName); initFromType(); } private void initFromType() { if (JSCommonTypeNames.BOOLEAN_CLASS_NAME.equals(type)) myEnumerated = true; if ("Function".equals(type)) myScriptable = true; } public void setRequired(final boolean isRequired) { myIsRequired = isRequired; } @Override public boolean isRequired() { return myIsRequired; } @Override public boolean isFixed() { return false; } @Override public boolean hasIdType() { return FlexMxmlLanguageAttributeNames.ID.equals(name); } @Override public boolean hasIdRefType() { return false; } @Override @Nullable public String getDefaultValue() { return null; } @Override public boolean isEnumerated() { return myEnumerated; } @Override public String[] getEnumeratedValues(@Nullable final XmlElement context) { if (context instanceof XmlAttribute && !myEnumerated && "id".equals(name)) { // id attribute value completion String value = ((XmlAttribute)context).getValue(); int index = value.indexOf(CompletionInitializationContext.DUMMY_IDENTIFIER); value = index == -1 ? value : value.substring(0, index); final PsiElement parent = context.getParent(); final String tagName = parent instanceof XmlTag ? ((XmlTag)parent).getLocalName() : null; if (StringUtil.isNotEmpty(tagName)) { final String[] suggestedIds = suggestIdValues(value, tagName); return makeUnique(suggestedIds, getNamedElementsVisibleAt(context)); } } return super.getEnumeratedValues(context); } @Override public String[] getEnumeratedValues() { @NonNls String[] enumerationValues = myEnumeratedValues; if (enumerationValues == null) { enumerationValues = ArrayUtil.EMPTY_STRING_ARRAY; if (myEnumerated) { if (JSCommonTypeNames.BOOLEAN_CLASS_NAME.equals(type)) { enumerationValues = TRUE_FALSE; } else { final PsiElement element = getDeclaration(); JSAttributeNameValuePair pair = null; if (element instanceof JSAttributeNameValuePair) { pair = ((JSAttribute)element.getParent()).getValueByName(ENUMERATION_ATTR_NAME); } else if (element instanceof JSAttributeListOwner) { JSAttribute inspectableAttr = findInspectableAttr(element); if (inspectableAttr != null) { pair = inspectableAttr.getValueByName(ENUMERATION_ATTR_NAME); } } if (pair != null) { final String simpleValue = pair.getSimpleValue(); if (simpleValue != null) { StringTokenizer tokenizer = new StringTokenizer(simpleValue, ", "); enumerationValues = new String[tokenizer.countTokens()]; int i = 0; while (tokenizer.hasMoreElements()) { enumerationValues[i++] = tokenizer.nextElement(); } } } } } myEnumeratedValues = enumerationValues; } return enumerationValues; } @Nullable public static JSAttribute findInspectableAttr(final PsiElement element) { return findAttr((JSAttributeListOwner)element, FlexAnnotationNames.INSPECTABLE); } @Nullable private static JSAttribute findAttr(final JSAttributeListOwner attributeListOwner, @NotNull @NonNls final String name) { final JSAttributeList attributeList = attributeListOwner != null ? attributeListOwner.getAttributeList() : null; JSAttribute[] attrs = attributeList != null ? attributeList.getAttributesByName(name) : null; final JSAttribute attribute = attrs != null && attrs.length > 0 ? attrs[0] : null; if (attribute == null && attributeList != null) { if (attributeListOwner instanceof JSFunction && ((JSFunction)attributeListOwner).isSetProperty()) { final PsiElement grandParent = attributeListOwner.getParent(); String propName = attributeListOwner.getName(); if (grandParent instanceof JSClass && propName != null) { final SinkResolveProcessor processor = new SinkResolveProcessor(propName, new ResolveResultSink(null, propName)); processor.setToProcessHierarchy(false); grandParent.processDeclarations(processor, ResolveState.initial(), grandParent, attributeListOwner); final List<PsiElement> elementList = processor.getResults(); if (elementList != null && elementList.size() == 2) { final PsiElement firstElement = elementList.get(0); final PsiElement secondElement = elementList.get(1); JSAttributeListOwner chosedElement = null; if (firstElement instanceof JSFunction && ((JSFunction)firstElement).isGetProperty()) { chosedElement = (JSAttributeListOwner)firstElement; } else if (secondElement instanceof JSFunction && ((JSFunction)secondElement).isGetProperty()) { chosedElement = (JSAttributeListOwner)secondElement; } if (chosedElement != null) { return findAttr(chosedElement, name); } } } } } return attribute; } @Override @Nullable public String validateValue(final XmlElement context, String value) { final PsiElement parent = context instanceof XmlAttributeValue ? context.getParent() : null; if (parent instanceof XmlAttribute && FlexMxmlLanguageAttributeNames.ID.equals(((XmlAttribute)parent).getName())) { final NamesValidator namesValidator = LanguageNamesValidation.INSTANCE.forLanguage(JavaScriptSupportLoader.JAVASCRIPT.getLanguage()); return namesValidator.isIdentifier(value, context.getProject()) ? null // ok : JSBundle.message("invalid.identifier.value"); } if (value.indexOf('{') != -1) return null; // dynamic values if (value.trim().startsWith("@Resource")) return null; if (myAnnotationName != null && CLEAR_DIRECTIVE.equals(value)) { return checkClearDirectiveContext(context); } if (isAllowsPercentage() && value.endsWith("%")) { value = value.substring(0, value.length() - 1); } boolean uint = false; if ("int".equals(type) || (uint = "uint".equals(type))) { try { boolean startWithSharp; if ((startWithSharp = value.startsWith("#")) || value.startsWith("0x")) { if (uint) { final long l = Long.parseLong(value.substring(startWithSharp ? 1 : 2), 16); if (l < 0 || l > 0xFFFFFFFFL) { throw new NumberFormatException("value out of range"); } } else { Integer.parseInt(value.substring(startWithSharp ? 1 : 2), 16); } } else { if ("Color".equals(format) && !StringUtil.isEmptyOrSpaces(value) && !Character.isDigit(value.charAt(0))) { return checkColorAlias(value); } if (uint) { final long l = Long.parseLong(value); if (l < 0 || l > 0xFFFFFFFFL) { throw new NumberFormatException("value out of range"); } } else { Integer.parseInt(value); } } } catch (NumberFormatException ex) { return FlexBundle.message("flex.invalid.integer.value"); } } else if ("Number".equals(type)) { try { boolean startWithSharp; if (value != null && ((startWithSharp = value.startsWith("#")) || value.startsWith("0x"))) { Integer.parseInt(value.substring(startWithSharp ? 1 : 2), 16); } else { Double.parseDouble(value); } } catch (NumberFormatException ex) { return FlexBundle.message("flex.invalid.number.value"); } } return null; } @Nullable private static String checkColorAlias(final String s) { if (!ArrayUtil.contains(s.toLowerCase(), COLOR_ALIASES)) { return FlexBundle.message("unknown.color.error", s); } return null; } @Nullable private static String checkClearDirectiveContext(final XmlElement context) { final PsiElement attributeOrTag = context instanceof XmlAttributeValue ? context.getParent() : context; final String name = attributeOrTag instanceof XmlAttribute ? ((XmlAttribute)attributeOrTag).getName() : attributeOrTag instanceof XmlTag ? ((XmlTag)attributeOrTag).getName() : null; if (name != null && !name.contains(".")) { return FlexBundle.message("clear.directive.state.specific.error"); } final PsiElement parent = attributeOrTag.getParent(); final XmlElementDescriptor descriptor = parent instanceof XmlTag ? ((XmlTag)parent).getDescriptor() : null; final PsiElement declaration = descriptor instanceof ClassBackedElementDescriptor ? descriptor.getDeclaration() : null; final PsiElement iStyleClient = ActionScriptClassResolver.findClassByQNameStatic(I_STYLE_CLIENT_CLASS, attributeOrTag); if (!(declaration instanceof JSClass) || !(iStyleClient instanceof JSClass) || !JSInheritanceUtil.isParentClass((JSClass)declaration, (JSClass)iStyleClient)) { return FlexBundle.message("clear.directive.IStyleClient.error"); } return null; } @Override @NonNls public String getName(final PsiElement context) { XmlTag tag = (XmlTag)context; if (tag.getName().indexOf("Rulezz") != -1) { // tag name completion final String namespaceByPrefix = tag.getPrefixByNamespace(parentDescriptor.context.namespace); if (namespaceByPrefix != null && !namespaceByPrefix.isEmpty()) return namespaceByPrefix + ":" + getName(); } return getName(); } @NotNull @Override public Object[] getDependences() { return ArrayUtil.EMPTY_OBJECT_ARRAY; } @Override public String getQualifiedName() { return getName(); } @Override public String getDefaultName() { return getName(); } @Override public XmlElementDescriptor[] getElementsDescriptors(final XmlTag context) { if (context.getDescriptor() instanceof ClassBackedElementDescriptor) { return parentDescriptor.getElementsDescriptors(context); } if (arrayElementType != null && ActionScriptClassResolver.findClassByQNameStatic(arrayElementType, context) != null) { return parentDescriptor.getElementDescriptorsInheritedFromGivenType(arrayElementType); } if (type != null && ClassBackedElementDescriptor.isAdequateType(type)) { return parentDescriptor.getElementDescriptorsInheritedFromGivenType(type); } if (ClassBackedElementDescriptor.IFACTORY_SHORT_CLASS_NAME.equals(ClassBackedElementDescriptor.className(type))) { return EMPTY_ARRAY; } return parentDescriptor.context.getDescriptorsWithAllowedDeclaration(); } @Override public XmlElementDescriptor getElementDescriptor(final XmlTag childTag, XmlTag contextTag) { if (MxmlJSClass.isTagThatAllowsAnyXmlContent(contextTag)) { return new AnyXmlElementWithAnyChildrenDescriptor(); } final String namespace = childTag.getNamespace(); XmlElementDescriptor descriptor; if (!ClassBackedElementDescriptor.sameNs(namespace, parentDescriptor.context.namespace)) { final XmlNSDescriptor nsdescriptor = childTag.getNSDescriptor(namespace, true); descriptor = nsdescriptor != null ? nsdescriptor.getElementDescriptor(childTag) : null; } else { descriptor = parentDescriptor.context.getElementDescriptor(childTag.getLocalName(), childTag); } if (arrayElementType != null) { if (descriptor == null || isVectorType(type) && isVectorDescriptor(descriptor) || JSCommonTypeNames.ARRAY_CLASS_NAME.equals(type) && isArrayDescriptor(descriptor) || ActionScriptClassResolver.findClassByQNameStatic(arrayElementType, childTag) == null) { return descriptor; } return ClassBackedElementDescriptor.checkValidDescriptorAccordingToType(arrayElementType, descriptor); } if (type != null && ClassBackedElementDescriptor.isAdequateType(type)) { return ClassBackedElementDescriptor.checkValidDescriptorAccordingToType(type, descriptor); } if (ClassBackedElementDescriptor.IFACTORY_SHORT_CLASS_NAME.equals(ClassBackedElementDescriptor.className(type))) { if (!FlexNameAlias.COMPONENT_TYPE_NAME.equals(childTag.getLocalName()) || !JavaScriptSupportLoader.isLanguageNamespace(childTag.getNamespace())) { return null; } } if (descriptor == null) { PsiElement element = getDeclaration(); if (element instanceof JSNamedElement) { element = new ClassBackedElementDescriptor(ClassBackedElementDescriptor.getPropertyType((JSNamedElement)element), parentDescriptor.context, parentDescriptor.project, true).getDeclaration(); return parentDescriptor.getClassIfDynamic(childTag.getLocalName(), element); } } return descriptor; } private static boolean isVectorType(final String type) { return type != null && (type.equals(JSCommonTypeNames.VECTOR_CLASS_NAME) || type.startsWith(JSCommonTypeNames.VECTOR_CLASS_NAME + ".<")); } private static boolean isVectorDescriptor(final XmlElementDescriptor descriptor) { return descriptor instanceof ClassBackedElementDescriptor && JSCommonTypeNames.VECTOR_CLASS_NAME.equals(descriptor.getName()) && JavaScriptSupportLoader.MXML_URI3.equals(((ClassBackedElementDescriptor)descriptor).context.namespace); } private static boolean isArrayDescriptor(final XmlElementDescriptor descriptor) { return descriptor instanceof ClassBackedElementDescriptor && JSCommonTypeNames.ARRAY_CLASS_NAME.equals(descriptor.getName()) && JavaScriptSupportLoader.MXML_URI3.equals(((ClassBackedElementDescriptor)descriptor).context.namespace); } @Override public XmlAttributeDescriptor[] getAttributesDescriptors(@Nullable final XmlTag context) { return XmlAttributeDescriptor.EMPTY; } @Override public XmlAttributeDescriptor getAttributeDescriptor(final String attributeName, @Nullable final XmlTag context) { return ClassBackedElementDescriptor.isPrivateAttribute(attributeName, context) ? new AnyXmlAttributeDescriptor(attributeName) : null; } @Override public XmlAttributeDescriptor getAttributeDescriptor(final XmlAttribute attribute) { return getAttributeDescriptor(attribute.getName(), attribute.getParent()); } @Override public XmlNSDescriptor getNSDescriptor() { return null; } @Override public XmlElementsGroup getTopGroup() { return null; } @Override public int getContentType() { return CONTENT_TYPE_UNKNOWN; } @Override public void validate(@NotNull final XmlElement context, @NotNull final ValidationHost host) { if (context instanceof XmlTag && FlexSdkUtils.isFlex4Sdk(FlexUtils.getSdkForActiveBC(parentDescriptor.context.module))) { MxmlLanguageTagsUtil.checkFlex4Attributes((XmlTag)context, host, false); } if (predefined) return; String value; if (context instanceof XmlTag) { value = ((XmlTag)context).getValue().getTrimmedText(); } else { value = ((XmlAttribute)context).getDisplayValue(); } final String message = validateValue(context, value); if (message != null) { PsiElement errorElement = context; if (context instanceof XmlTag) { final XmlText[] textElements = ((XmlTag)context).getValue().getTextElements(); if (textElements.length == 1 && !StringUtil.isEmptyOrSpaces(textElements[0].getText())) { errorElement = textElements[0]; } } host.addMessage(errorElement, message, ValidationHost.ErrorType.ERROR); } if (context instanceof XmlTag && FlexStateElementNames.STATES.equals(((XmlTag)context).getLocalName()) && FlexUtils.isMxmlNs(((XmlTag)context).getNamespace())) { XmlTag[] tags = ((XmlTag)context).findSubTags("State", ((XmlTag)context).getNamespace()); XmlUtil.doDuplicationCheckForElements(tags, new HashMap<>(tags.length), new XmlUtil.DuplicationInfoProvider<XmlTag>() { @Override public String getName(@NotNull XmlTag xmlTag) { return xmlTag.getAttributeValue(FlexStateElementNames.NAME); } @Override @NotNull public String getNameKey(@NotNull XmlTag xmlTag, @NotNull String name) { return getName(xmlTag); } @Override @NotNull public PsiElement getNodeForMessage(@NotNull XmlTag xmlTag) { return xmlTag.getAttribute(FlexStateElementNames.NAME).getValueElement(); } }, host); } } @Override public boolean requiresCdataBracesInContext(@NotNull final XmlTag context) { return myScriptable; } @Override public String getType() { return type; } @Override public String getArrayType() { return arrayElementType; } @Override public boolean allowElementsFromNamespace(String namespace, XmlTag context) { //if (type.equals("mx.core.IFactory") && JavaScriptSupportLoader.isLanguageNamespace(namespace)) { // return true; //} //if (type != null || arrayElementType != null) { // return true; //} return true; } @Override public String getTypeName() { return myProperty ? "Function" : myAnnotationName; } @Override public Icon getIcon() { if (myProperty) { return PlatformIcons.PROPERTY_ICON; } if (myAnnotationName == null) return null; if (myAnnotationName.equals(FlexAnnotationNames.EVENT)) { return PlatformIcons.EXCEPTION_CLASS_ICON; } if (myAnnotationName.equals(FlexAnnotationNames.STYLE)) { return AllIcons.FileTypes.Css; } if (myAnnotationName.equals(FlexAnnotationNames.EFFECT)) { return AllIcons.Actions.Lightning; } return null; } @Override public boolean isPredefined() { return predefined; } @Override public boolean isAllowsPercentage() { return percentProxy != null; } @Override public String getPercentProxy() { return percentProxy; } @Override public boolean isStyle() { return myAnnotationName != null && !myScriptable && myAnnotationName.equals(FlexAnnotationNames.STYLE); } @Override public boolean isRichTextContent() { return myRichTextContent; } @Override public boolean isCollapseWhiteSpace() { return myCollapseWhiteSpace; } @Override public boolean isDeferredInstance() { return myDeferredInstance; } @Override public boolean contentIsArrayable() { return type != null && (type.equals(JSCommonTypeNames.ARRAY_CLASS_NAME) || type.equals(JSCommonTypeNames.OBJECT_CLASS_NAME) || type.equals(JSCommonTypeNames.ANY_TYPE)); } public static String[] suggestIdValues(final String value, final String type) { final String[] typeParts = getTypeParts(type); final String[] result = new String[typeParts.length]; for (int i = 0; i < result.length; i++) { // merge written text with suggestions: "myBu" + "Button" -> "myButton" final String lowercasedStart = lowercaseStart(typeParts[i]); if (value.isEmpty() || lowercasedStart.startsWith(value)) { result[i] = lowercasedStart; } else { int j = 0; for (; j < value.length(); j++) { String s = value.substring(j); if (typeParts[i].startsWith(s)) { break; } } final int commonTextLength = value.length() - j; result[i] = value.substring(0, value.length() - commonTextLength) + typeParts[i]; } } return result; } private static String[] getTypeParts(final String type) { // HTTPService -> HTTPService, Service // ButtonBarButton -> ButtonBarButton, BarButton, Button final List<String> result = new LinkedList<>(); result.add(Character.toUpperCase(type.charAt(0)) + (type.length() > 1 ? type.substring(1) : "")); for (int i = 1; i < type.length() - 1; i++) { if (Character.isUpperCase(type.charAt(i)) && !Character.isUpperCase(type.charAt(i + 1))) { result.add(type.substring(i)); } } return result.toArray(new String[result.size()]); } private static String lowercaseStart(final String s) { // URL -> url // HTTPService -> httpService // ButtonBar -> buttonBar int i = 0; while (i < s.length() && Character.isUpperCase(s.charAt(i))) { i++; } i = i <= 1 ? 1 : i == s.length() ? i : i - 1; return s.substring(0, i).toLowerCase() + s.substring(i); } private static Set<String> getNamedElementsVisibleAt(@NotNull final PsiElement context) { final Set<String> names = new HashSet<>(); ResolveProcessor processor = new ResolveProcessor(null) { @Override public boolean execute(@NotNull final PsiElement element, @NotNull final ResolveState state) { if (element instanceof JSNamedElementBase) { names.add(((JSNamedElementBase)element).getName()); } return true; } }; processor.setLocalResolve(true); JSResolveUtil.treeWalkUp(processor, context, context.getParent(), context); return names; } private static String[] makeUnique(final String[] names, final Set<String> existingNames) { for (int i = 0; i < names.length; i++) { String name = names[i]; int postfix = 2; while (existingNames.contains(name)) { name = names[i] + postfix++; } names[i] = name; } return names; } }