/* * Copyright 2009 CoreMedia AG * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an "AS * IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either * express or implied. See the License for the specific language * governing permissions and limitations under the License. */ package net.jangaroo.ide.idea.exml; import com.intellij.idea.IdeaLogger; import com.intellij.lang.javascript.JavaScriptSupportLoader; import com.intellij.lang.javascript.psi.JSFunction; import com.intellij.lang.javascript.psi.JSParameter; import com.intellij.lang.javascript.psi.JSType; import com.intellij.lang.javascript.psi.JSVariable; import com.intellij.lang.javascript.psi.ecmal4.JSClass; import com.intellij.lang.javascript.psi.resolve.ResolveResultSink; import com.intellij.lang.javascript.psi.resolve.SinkResolveProcessor; import com.intellij.openapi.diagnostic.Logger; import com.intellij.openapi.module.Module; import com.intellij.openapi.progress.ProcessCanceledException; import com.intellij.openapi.util.TextRange; import com.intellij.openapi.vfs.VirtualFile; import com.intellij.psi.InjectedLanguagePlaces; import com.intellij.psi.LanguageInjector; import com.intellij.psi.LiteralTextEscaper; import com.intellij.psi.PsiElement; import com.intellij.psi.PsiFile; import com.intellij.psi.PsiLanguageInjectionHost; import com.intellij.psi.ResolveState; import com.intellij.psi.xml.XmlAttribute; import com.intellij.psi.xml.XmlAttributeValue; import com.intellij.psi.xml.XmlFile; import com.intellij.psi.xml.XmlTag; import com.intellij.psi.xml.XmlText; import net.jangaroo.exml.api.Exmlc; import net.jangaroo.exml.utils.ExmlUtils; import net.jangaroo.ide.idea.Utils; import net.jangaroo.ide.idea.jps.exml.ExmlcConfigurationBean; import net.jangaroo.utils.AS3Type; import net.jangaroo.utils.CompilerUtils; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.util.ArrayList; import java.util.LinkedHashSet; import java.util.List; import java.util.Set; /** * AS3 language injection in EXML files. */ public class ExmlLanguageInjector implements LanguageInjector { private static final Method GET_PARAMETERS; static { Method getParameters = null; try { // try IDEA 2016 version: getParameterVariables() returns JSParameter[], while getParameters() has been // changed to return JSParameterListElement[] :-( getParameters = JSFunction.class.getMethod("getParameterVariables"); } catch (NoSuchMethodException e) { // it is not there: fall back to IDEA 15 version. try { getParameters = JSFunction.class.getMethod("getParameters"); } catch (NoSuchMethodException e1) { IdeaLogger.getInstance(ExmlLanguageInjector.class).error("Jangaroo: Incompatible IDEA version: Neither JSFunction#getParameterVariables() nor #getParameters() has been found.", e1); e1.printStackTrace(); } } GET_PARAMETERS = getParameters; } public void getLanguagesToInject(@NotNull PsiLanguageInjectionHost psiLanguageInjectionHost, @NotNull InjectedLanguagePlaces injectedLanguagePlaces) { PsiFile psiFile = psiLanguageInjectionHost.getContainingFile(); if (psiFile.getName().endsWith(Exmlc.EXML_SUFFIX) && (psiLanguageInjectionHost instanceof XmlAttributeValue || psiLanguageInjectionHost instanceof XmlText)) { VirtualFile exmlFile = psiFile.getOriginalFile().getVirtualFile(); if (exmlFile == null) { return; } Module module = Utils.getModuleForFile(psiFile.getProject(), exmlFile); if (module == null) { return; } ExmlcConfigurationBean exmlConfig = ExmlFacet.getExmlConfig(module); if (exmlConfig == null) { return; } try { if (psiLanguageInjectionHost instanceof XmlAttributeValue) { XmlAttributeValue attributeValue = (XmlAttributeValue)psiLanguageInjectionHost; String text = attributeValue.getValue(); if (isImportClassAttribute(attributeValue)) { injectedLanguagePlaces.addPlace(JavaScriptSupportLoader.ECMA_SCRIPT_L4, TextRange.from(1, text.length()), "import ", ";"); } else { if (isBaseClassAttribute(attributeValue) || isDeclarationTypeAttribute(attributeValue) || isDeclarationValueAttribute(attributeValue) || CompilerUtils.isCodeExpression(text)) { injectAS(injectedLanguagePlaces, exmlFile, module, exmlConfig, psiLanguageInjectionHost); } } } else { // psiLanguageInjectionHost instanceof XmlText XmlText xmlText = (XmlText)psiLanguageInjectionHost; XmlTag parentTag = xmlText.getParentTag(); if (isExmlElement(parentTag, Exmlc.EXML_ANNOTATION_NODE_NAME)) { injectedLanguagePlaces.addPlace(JavaScriptSupportLoader.ECMA_SCRIPT_L4, TextRange.from(0, xmlText.getTextRange().getLength()), "[", "]"); } else if (isExmlElement(parentTag, Exmlc.EXML_OBJECT_NODE_NAME)) { injectAS(injectedLanguagePlaces, exmlFile, module, exmlConfig, psiLanguageInjectionHost); } else if (isExmlElement(parentTag, Exmlc.EXML_DESCRIPTION_NODE_NAME)) { injectedLanguagePlaces.addPlace(JavaScriptSupportLoader.ECMA_SCRIPT_L4, TextRange.from(0, xmlText.getTextRange().getLength()), "/**", "*/"); } else { injectAS(injectedLanguagePlaces, exmlFile, module, exmlConfig, psiLanguageInjectionHost); } } } catch (Throwable t) { if (!(t instanceof ProcessCanceledException)) { Logger.getInstance("ExmlLanguageInjector").error("While trying to inject AS3 into " + exmlFile.getPath() + ".", t); } } } } private void injectAS(InjectedLanguagePlaces injectedLanguagePlaces, VirtualFile exmlFile, Module module, ExmlcConfigurationBean exmlConfig, PsiLanguageInjectionHost attributeValue) { String configClassPackage = exmlConfig.getConfigClassPackage(); if (configClassPackage == null) { IdeaLogger.getInstance(this.getClass()).warn("No config class package set in module " + module.getName() + ", EXML AS3 language injection cancelled."); return; } XmlTag exmlComponentTag = ((XmlFile)attributeValue.getContainingFile()).getRootTag(); if (exmlComponentTag != null) { String text; if (attributeValue instanceof XmlAttributeValue) { text = ((XmlAttributeValue)attributeValue).getValue(); } else { text = getRelevantText(attributeValue); if (text.trim().length() == 0) { // ignore white-space-only text nodes return; } } boolean isCodeExpression = CompilerUtils.isCodeExpression(text); // find relative path to source root to determine package name: VirtualFile packageDir = exmlFile.getParent(); String packageName = packageDir == null ? "" : Utils.getModuleRelativeSourcePath(module.getProject(), packageDir, '.'); String className = exmlFile.getNameWithoutExtension(); StringBuilder code = new StringBuilder(); code.append(String.format("package %s {\n", packageName)); String superClassName = isCodeExpression || attributeValue instanceof XmlText ? findSuperClass(exmlComponentTag) : null; String configClassName = CompilerUtils.qName(configClassPackage, CompilerUtils.uncapitalize(className)); // find and append imports: Set<String> imports = findImports(exmlComponentTag); if (superClassName != null) { imports.add(superClassName); } imports.add(configClassName); for (String importName : imports) { code.append(String.format("import %s;\n", importName)); } code.append(String.format("public class %s", className)); String codePrefix = null; code.append(" extends "); if (attributeValue instanceof XmlAttributeValue && isBaseClassAttribute((XmlAttributeValue)attributeValue)) { codePrefix = flush(code); } else { code.append(superClassName != null ? superClassName : "Object"); } code.append(" {\n"); // determine the current EXML element: XmlAttribute xmlAttribute = getXmlAttribute(attributeValue); XmlTag xmlTag = xmlAttribute != null ? xmlAttribute.getParent() : null; if (codePrefix == null && xmlTag != null && attributeValue instanceof XmlAttributeValue && isAttribute((XmlAttributeValue)attributeValue, Exmlc.EXML_CFG_NODE_NAME, Exmlc.EXML_CFG_TYPE_ATTRIBUTE)) { String cfgName = xmlTag.getAttributeValue(Exmlc.EXML_DECLARATION_NAME_ATTRIBUTE); if (cfgName == null || cfgName.isEmpty()) { cfgName = "__anon__"; } code.append(String.format("public var %s:", cfgName)); codePrefix = flush(code); code.append(";"); } if (codePrefix == null) { codePrefix = renderDeclarations(exmlComponentTag, code, xmlTag, xmlAttribute, Exmlc.EXML_CONSTANT_NODE_NAME, "public static var"); // "var" to allow later assignment for complex value! } code.append(String.format("public function %s(config:%s = null){\n", className, configClassName)); if (codePrefix == null) { codePrefix = renderDeclarations(exmlComponentTag, code, xmlTag, xmlAttribute, Exmlc.EXML_VAR_NODE_NAME, "var"); } if (codePrefix == null) { String attributeName = null; if (xmlTag != null) { attributeName = xmlAttribute.getLocalName(); if (Exmlc.EXML_UNTYPED_NAMESPACE_URI.equals(xmlAttribute.getNamespace())) { xmlTag = null; } } else if (attributeValue instanceof XmlText) { XmlTag parentTag = ((XmlText)attributeValue).getParentTag(); if (parentTag != null) { final boolean insideExmlObject = isExmlElement(parentTag, Exmlc.EXML_OBJECT_NODE_NAME); if (!insideExmlObject && !isCodeExpression) { return; } XmlTag attributeTag = insideExmlObject ? parentTag.getParentTag() : parentTag; if (attributeTag != null) { attributeName = attributeTag.getLocalName(); if (attributeTag.getSubTags().length > 1) { // the code inside the <exml:object> element is wrapped by an Array: do not check the type! xmlTag = null; } else { xmlTag = attributeTag.getParentTag(); } } } } if (attributeName == null) { // not inside an attribute element: bail out return; } String attributeConfigClassName = "Object"; if (xmlTag != null) { String configPackageName = ExmlUtils.parsePackageFromNamespace(xmlTag.getNamespace()); if (configPackageName != null) { attributeConfigClassName = CompilerUtils.qName(configPackageName, xmlTag.getLocalName()); // since EXML update, this may be a target class, so try to find the reference to the config class: JSClass asClass = Utils.getActionScriptClass(xmlTag, attributeConfigClassName); if (asClass != null) { JSFunction asConstructor = asClass.getConstructor(); if (asConstructor != null) { JSParameter[] parameters = getParameters(asConstructor); if (parameters.length > 0 & "config".equals(parameters[0].getName())) { JSType configClassCandidate = parameters[0].getType(); if (configClassCandidate != null) { String configClassNameCandidate = configClassCandidate.getResolvedTypeText(); if (!"Object".equals(configClassNameCandidate)) { attributeConfigClassName = configClassNameCandidate; asClass = Utils.getActionScriptClass(xmlTag, attributeConfigClassName); } } } } } if (attributeValue instanceof XmlText && asClass != null) { // check whether type of config attribute is "Array", then disable type check as Arrays can hold anything: // find declaration of "attributeName" get or set method: SinkResolveProcessor propertyResolveProcessor = new SinkResolveProcessor<ResolveResultSink>(new ResolveResultSink(asClass, attributeName)); propertyResolveProcessor.setToProcessHierarchy(true); propertyResolveProcessor.setToProcessMembers(true); if (!asClass.processDeclarations(propertyResolveProcessor, ResolveState.initial(), asClass, asClass)) { PsiElement result = propertyResolveProcessor.getResult(); final String propertyType; if (result instanceof JSFunction) { JSFunction method = (JSFunction)result; propertyType = method.isSetProperty() ? getTypeFromSetAccessor(method) : method.getReturnTypeString(); } else if (result instanceof JSVariable) { propertyType = ((JSVariable)result).getTypeString(); } else { propertyType = null; } if ("Array".equals(propertyType)) { // found Array-typed property: stop processing and return false! // disable type check by falling back to Object type: attributeConfigClassName = "Object"; } } } } } if (xmlTag != null && (isExmlElement(xmlTag, Exmlc.EXML_VAR_NODE_NAME) || isExmlElement(xmlTag, Exmlc.EXML_CONSTANT_NODE_NAME))) { code.append(xmlTag.getAttributeValue(Exmlc.EXML_DECLARATION_NAME_ATTRIBUTE)); } else if (xmlTag != null && isExmlElement(xmlTag, Exmlc.EXML_CFG_NODE_NAME)) { String cfgType = xmlTag.getAttributeValue(Exmlc.EXML_CFG_TYPE_ATTRIBUTE); if (cfgType == null || cfgType.isEmpty()) { cfgType = AS3Type.ANY.toString(); } code.append("var ").append(xmlTag.getAttributeValue(Exmlc.EXML_CFG_NAME_ATTRIBUTE)) .append(":").append(cfgType); } else { code.append(" new ").append(attributeConfigClassName).append("().").append(attributeName); } code.append(" = "); codePrefix = flush(code); code.append(";"); } code.append(" super(config);"); // causes exceptions in IDEA 13 when superclass has no 1-arg-constructor! code .append("}\n") // constructor { .append("\n}\n") // class { .append("\n}\n"); // package { TextRange textRange = attributeValue instanceof XmlText ? attributeValue.createLiteralTextEscaper().getRelevantTextRange() : TextRange.from(1, text.length()); // cut off quotes ("...") if (isCodeExpression) { textRange = textRange.shiftRight(1).grown(-2); // cut off braces ({...}) } injectedLanguagePlaces.addPlace(JavaScriptSupportLoader.ECMA_SCRIPT_L4, textRange, codePrefix, code.toString()); } } @NotNull private static JSParameter[] getParameters(JSFunction fun) { if (GET_PARAMETERS == null) { return new JSParameter[0]; } try { return (JSParameter[])GET_PARAMETERS.invoke(fun); } catch (IllegalAccessException e) { throw new RuntimeException("Jangaroo: Error while retrieving function parameters.", e); } catch (InvocationTargetException e) { throw new RuntimeException("Jangaroo: Error while retrieving function parameters.", e); } } /* * Do *not* reuse JSResolveUtil, because there are incompatible API changes between IDEA 14.0 and 14.1! */ @Nullable private static String getTypeFromSetAccessor(JSFunction fun) { JSParameter[] jsParameters = getParameters(fun); JSParameter parameter = jsParameters.length == 1 ? jsParameters[0] : null; return parameter != null ? parameter.getTypeString() : null; } private String getRelevantText(PsiLanguageInjectionHost languageInjectionHost) { String text;LiteralTextEscaper<? extends PsiLanguageInjectionHost> literalTextEscaper = languageInjectionHost.createLiteralTextEscaper(); StringBuilder builder = new StringBuilder(); literalTextEscaper.decode(literalTextEscaper.getRelevantTextRange(), builder); text = builder.toString(); return text; } private String renderDeclarations(XmlTag exmlComponentTag, StringBuilder code, XmlTag xmlTag, XmlAttribute xmlAttribute, String nodeName, String declarationPrefix) { int editingIndex = 0; if (xmlTag != null && isExmlElement(xmlTag, nodeName)) { String attributeName = xmlAttribute.getLocalName(); if (Exmlc.EXML_DECLARATION_TYPE_ATTRIBUTE.equals(attributeName)) { editingIndex = 1; // second element in String[]: type } else if (Exmlc.EXML_DECLARATION_VALUE_ATTRIBUTE.equals(attributeName)) { editingIndex = 2; // third element in String[]: value } } // find and append declarations: List<String[]> declarations = findDeclarationsUntil(exmlComponentTag, editingIndex > 0 ? xmlTag : null, nodeName); return renderDeclarations(code, declarations, declarationPrefix, editingIndex); } private String renderDeclarations(StringBuilder code, List<String[]> constants, String declarationPrefix, int editingIndex) { String codePrefix = null; for (int i = 0, constantsSize = constants.size(); i < constantsSize; i++) { String[] constantNameTypeValue = constants.get(i); String description = constantNameTypeValue[3]; if (description != null) { code.append("/** ").append(description).append(" */"); } code.append(declarationPrefix).append(' '); String name = constantNameTypeValue[0]; code.append(name); String type = constantNameTypeValue[1]; if (i == constantsSize - 1 && editingIndex == 1) { code.append(':'); codePrefix = flush(code); } else { if (type != null) { code.append(':').append(type); } } code.append(" = "); if (i == constantsSize - 1 && editingIndex == 2) { codePrefix = flush(code); } else { String value = constantNameTypeValue[2]; if (value == null) { code.append("undefined"); // prevent "variable might not have been initialized"! } else { if (CompilerUtils.isCodeExpression(value)) { value = CompilerUtils.getCodeExpression(value); } code.append(value); } } code.append(";\n"); } return codePrefix; } private static String flush(StringBuilder sb) { String current = sb.toString(); sb.setLength(0); return current; } private static String findSuperClass(XmlTag exmlComponentTag) { XmlAttribute baseClassAttribute = exmlComponentTag.getAttribute(Exmlc.EXML_BASE_CLASS_ATTRIBUTE); if (baseClassAttribute != null) { return baseClassAttribute.getValue(); } XmlTag[] subTags = exmlComponentTag.getSubTags(); XmlTag componentTag = findNonExmlNamespaceTag(subTags); if (componentTag != null) { return ExmlElementGotoDeclarationHandler.findTargetClassName(componentTag); } return null; } private static XmlTag findNonExmlNamespaceTag(XmlTag[] subTags) { for (int i = subTags.length - 1; i >= 0; i--) { if (!isExmlElement(subTags[i])) { return subTags[i]; } } return null; } private static Set<String> findImports(XmlTag exmlComponentTag) { Set<String> imports = new LinkedHashSet<String>(); XmlTag componentTag = null; for (XmlTag topLevelXmlTag : exmlComponentTag.getSubTags()) { if (isExmlElement(topLevelXmlTag)) { String elementName = topLevelXmlTag.getLocalName(); if (Exmlc.EXML_IMPORT_NODE_NAME.equals(elementName)) { imports.add(topLevelXmlTag.getAttributeValue(Exmlc.EXML_IMPORT_CLASS_ATTRIBUTE)); } else if (Exmlc.EXML_CONSTANT_NODE_NAME.equals(elementName) || Exmlc.EXML_CFG_NODE_NAME.equals(elementName) || Exmlc.EXML_VAR_NODE_NAME.equals(elementName)) { String type = topLevelXmlTag.getAttributeValue(Exmlc.EXML_DECLARATION_TYPE_ATTRIBUTE); if (type != null && type.contains(".")) { imports.add(type); } } } else { componentTag = topLevelXmlTag; // remember last non-EXML-namespace tag, which contains the view tree } } if (componentTag != null) { addComponentImports(imports, componentTag); } return imports; } private static void addComponentImports(Set<String> imports, XmlTag componentTag) { String packageName = ExmlUtils.parsePackageFromNamespace(componentTag.getNamespace()); if (packageName != null) { String configClassName = CompilerUtils.qName(packageName, componentTag.getLocalName()); imports.add(configClassName); for (XmlTag property : componentTag.getSubTags()) { for (XmlTag subComponent : property.getSubTags()) { addComponentImports(imports, subComponent); } } } } private static List<String[]> findDeclarationsUntil(XmlTag exmlComponentTag, XmlTag untilNode, String nodeName) { List<String[]> constants = new ArrayList<String[]>(); for (XmlTag topLevelXmlTag : exmlComponentTag.getSubTags()) { if (nodeName.equals(topLevelXmlTag.getLocalName())) { String name = topLevelXmlTag.getAttributeValue(Exmlc.EXML_DECLARATION_NAME_ATTRIBUTE); if (name == null) { continue; } String type = topLevelXmlTag.getAttributeValue(Exmlc.EXML_DECLARATION_TYPE_ATTRIBUTE); String attributeValue = topLevelXmlTag.getAttributeValue(Exmlc.EXML_DECLARATION_VALUE_ATTRIBUTE); if (type == null) { AS3Type as3Type = attributeValue == null ? null : CompilerUtils.guessType(attributeValue); if (as3Type == null) { as3Type = AS3Type.STRING; } type = as3Type.toString(); } XmlTag[] descriptionTags = topLevelXmlTag.findSubTags(Exmlc.EXML_DESCRIPTION_NODE_NAME, Exmlc.EXML_NAMESPACE_URI); String description = descriptionTags.length > 0 ? descriptionTags[0].getValue().getText() : null; constants.add(new String[]{ name, type, attributeValue, description }); if (topLevelXmlTag.equals(untilNode)) { break; } } } return constants; } private static boolean isImportClassAttribute(XmlAttributeValue attributeValue) { return isAttribute(attributeValue, Exmlc.EXML_IMPORT_NODE_NAME, Exmlc.EXML_IMPORT_CLASS_ATTRIBUTE); } private static boolean isDeclarationTypeAttribute(XmlAttributeValue attributeValue) { return isAttribute(attributeValue, null, Exmlc.EXML_DECLARATION_TYPE_ATTRIBUTE); } private static boolean isDeclarationValueAttribute(XmlAttributeValue attributeValue) { if (isAttribute(attributeValue, null, Exmlc.EXML_DECLARATION_VALUE_ATTRIBUTE)) { // only return "true" if there also is a non-empty declaration type attribute, because otherwise, // language injection makes no sense: XmlTag declarationElement = (XmlTag)attributeValue.getParent().getParent(); String type = declarationElement.getAttributeValue(Exmlc.EXML_DECLARATION_TYPE_ATTRIBUTE); return type != null && type.length() > 0 && !AS3Type.STRING.toString().equals(type); } return false; } private static boolean isBaseClassAttribute(XmlAttributeValue attributeValue) { return isAttribute(attributeValue, null, Exmlc.EXML_BASE_CLASS_ATTRIBUTE); } private static boolean isExmlElement(XmlTag element) { return Exmlc.EXML_NAMESPACE_URI.equals(element.getNamespace()); } private static boolean isExmlElement(XmlTag element, @Nullable String exmlElementName) { return isExmlElement(element) && (exmlElementName == null || exmlElementName.equals(element.getLocalName())); } private static boolean isAttribute(XmlAttributeValue attributeValue, @Nullable String exmlElementName, String exmlAttribute) { XmlAttribute attribute = getXmlAttribute(attributeValue); if (attribute != null && exmlAttribute.equals(attribute.getName())) { XmlTag element = attribute.getParent(); return isExmlElement(element, exmlElementName); } return false; } private static XmlAttribute getXmlAttribute(PsiElement attributeValue) { PsiElement parent = attributeValue.getParent(); return parent instanceof XmlAttribute ? (XmlAttribute)parent : null; } }