package org.angularjs.index;
import com.intellij.codeInsight.completion.CompletionUtil;
import com.intellij.lang.ASTNode;
import com.intellij.lang.injection.InjectedLanguageManager;
import com.intellij.lang.javascript.*;
import com.intellij.lang.javascript.frameworks.jquery.JQueryCssLanguage;
import com.intellij.lang.javascript.index.FrameworkIndexingHandler;
import com.intellij.lang.javascript.psi.*;
import com.intellij.lang.javascript.psi.ecma6.ES6Decorator;
import com.intellij.lang.javascript.psi.ecmal4.JSClass;
import com.intellij.lang.javascript.psi.literal.JSLiteralImplicitElementCustomProvider;
import com.intellij.lang.javascript.psi.resolve.BaseJSSymbolProcessor;
import com.intellij.lang.javascript.psi.stubs.JSElementIndexingData;
import com.intellij.lang.javascript.psi.stubs.JSImplicitElement;
import com.intellij.lang.javascript.psi.stubs.JSImplicitElementStructure;
import com.intellij.lang.javascript.psi.stubs.impl.JSElementIndexingDataImpl;
import com.intellij.lang.javascript.psi.stubs.impl.JSImplicitElementImpl;
import com.intellij.lang.javascript.psi.types.JSContext;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.psi.PsiDirectory;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiFile;
import com.intellij.psi.PsiFileFactory;
import com.intellij.psi.css.*;
import com.intellij.psi.impl.source.resolve.FileContextUtil;
import com.intellij.psi.impl.source.tree.LeafElement;
import com.intellij.psi.impl.source.tree.TreeUtil;
import com.intellij.psi.stubs.IndexSink;
import com.intellij.psi.util.PsiTreeUtil;
import com.intellij.util.ObjectUtils;
import com.intellij.util.containers.MultiMap;
import org.angularjs.html.Angular2HTMLLanguage;
import org.angularjs.lang.AngularJSLanguage;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.*;
import static org.angularjs.index.AngularJSIndexingHandler.ANGULAR_DIRECTIVES_INDEX_USER_STRING;
import static org.angularjs.index.AngularJSIndexingHandler.ANGULAR_FILTER_INDEX_USER_STRING;
public class AngularJS2IndexingHandler extends FrameworkIndexingHandler {
public static final String TEMPLATE_REF = "TemplateRef";
public static final String SELECTOR = "selector";
public static final String NAME = "name";
public static final String DECORATORS = "adei";
public static final String DECORATE = "__decorate";
static {
JSImplicitElement.ourUserStringsRegistry.registerUserString(DECORATORS);
}
@Override
public void processCallExpression(JSCallExpression callExpression, @NotNull JSElementIndexingData outData) {
final JSExpression expression = callExpression.getMethodExpression();
if (expression instanceof JSReferenceExpression) {
final String name = ((JSReferenceExpression)expression).getReferenceName();
if (isDirective(name)) {
addImplicitElement(callExpression, (JSElementIndexingDataImpl)outData, getPropertyName(callExpression, SELECTOR));
}
if (isPipe(name)) {
addPipe(callExpression, (JSElementIndexingDataImpl)outData, getPropertyName(callExpression, NAME));
}
if (isModule(name)) {
addImplicitElementToModules(callExpression, (JSElementIndexingDataImpl)outData, determineModuleName(callExpression));
}
}
}
@Nullable
@Override
public JSElementIndexingData processAnyProperty(@NotNull JSProperty property, @Nullable JSElementIndexingData outData) {
if ("args".equals(property.getName())) {
final JSObjectLiteralExpression object = (JSObjectLiteralExpression)property.getParent();
final JSProperty type = object.findProperty("type");
if (type != null) {
final JSExpression value = type.getValue();
if (value instanceof JSReferenceExpression && isDirective(((JSReferenceExpression)value).getReferenceName())) {
return addImplicitElement(property, (JSElementIndexingDataImpl)outData, getPropertyName(property, SELECTOR));
}
}
}
return super.processAnyProperty(property, outData);
}
@Override
public boolean shouldCreateStubForCallExpression(ASTNode node) {
ASTNode ref = node.getFirstChildNode();
if (ref.getElementType() == JSTokenTypes.NEW_KEYWORD) {
ref = TreeUtil.findSibling(ref, JSElementTypes.REFERENCE_EXPRESSION);
}
if (ref != null){
final ASTNode name = ref.getLastChildNode();
if (name != null && name.getElementType() == JSTokenTypes.IDENTIFIER) {
final String referencedName = name.getText();
return isDirective(referencedName) || isPipe(referencedName) || isModule(referencedName);
}
}
return false;
}
private static String determineModuleName(@NotNull JSCallExpression callExpression) {
if (!(callExpression.getParent() instanceof ES6Decorator)) return null;
final ES6Decorator decorator = (ES6Decorator)callExpression.getParent();
final PsiElement owner = decorator.getOwner();
if (owner instanceof JSClass) return ((JSClass)owner).getName();
return null;
}
private static void addImplicitElementToModules(PsiElement decorator,
@NotNull JSElementIndexingDataImpl outData,
String selector) {
if (selector == null) return;
JSImplicitElementImpl.Builder elementBuilder = new JSImplicitElementImpl.Builder(selector, decorator)
.setUserString(AngularJSIndexingHandler.ANGULAR_MODULE_INDEX_USER_STRING);
outData.addImplicitElement(elementBuilder.toImplicitElement());
}
private static JSElementIndexingDataImpl addImplicitElement(PsiElement element,
JSElementIndexingDataImpl outData,
String selector) {
if (selector == null) return outData;
selector = selector.replace("\\n", "\n");
final MultiMap<String, String> attributesToElements = MultiMap.createSet();
PsiFile cssFile = PsiFileFactory.getInstance(element.getProject()).createFileFromText(JQueryCssLanguage.INSTANCE, selector);
CssSelectorList selectorList = PsiTreeUtil.findChildOfType(cssFile, CssSelectorList.class);
if (selectorList == null) return outData;
for (CssSelector cssSelector : selectorList.getSelectors()) {
for (CssSimpleSelector simpleSelector : cssSelector.getSimpleSelectors()) {
String elementName = simpleSelector.getElementName();
boolean seenAttribute = false;
for (CssSelectorSuffix suffix : simpleSelector.getSelectorSuffixes()) {
if (!(suffix instanceof CssAttribute)) continue;
String name = ((CssAttribute)suffix).getAttributeName();
if (!StringUtil.isEmpty(name)) {
if (seenAttribute) name = "[" + name + "]";
attributesToElements.putValue(name, elementName);
}
seenAttribute = true;
}
if (!seenAttribute) attributesToElements.putValue("", elementName);
}
}
Set<String> added = new HashSet<>();
boolean template = isTemplate(element);
for (String elementName : attributesToElements.get("")) {
if (!added.add(elementName)) continue;
JSImplicitElementImpl.Builder elementBuilder = new JSImplicitElementImpl.Builder(elementName, element)
.setType(JSImplicitElement.Type.Class);
if (!attributesToElements.containsKey(elementName)) {
elementBuilder.setTypeString("E;;;");
} else {
Collection<String> elements = attributesToElements.get(elementName);
elementBuilder.setTypeString("AE;" + StringUtil.join(elements, ",") + ";;");
}
elementBuilder.setUserString(ANGULAR_DIRECTIVES_INDEX_USER_STRING);
if (outData == null) outData = new JSElementIndexingDataImpl();
outData.addImplicitElement(elementBuilder.toImplicitElement());
}
for (Map.Entry<String, Collection<String>> entry : attributesToElements.entrySet()) {
JSImplicitElementImpl.Builder elementBuilder;
String attributeName = entry.getKey();
if (attributeName.isEmpty()) {
continue;
}
if (!added.add(attributeName)) continue;
if (outData == null) outData = new JSElementIndexingDataImpl();
String elements = StringUtil.join(entry.getValue(), ",");
if (template && elements.isEmpty()) {
elementBuilder = new JSImplicitElementImpl.Builder(attributeName, element)
.setType(JSImplicitElement.Type.Class).setTypeString("A;template,ng-template;;");
elementBuilder.setUserString(ANGULAR_DIRECTIVES_INDEX_USER_STRING);
outData.addImplicitElement(elementBuilder.toImplicitElement());
}
final String prefix = isTemplate(element) && !attributeName.startsWith("[") ? "*" : "";
final String attr = prefix + attributeName;
elementBuilder = new JSImplicitElementImpl.Builder(attr, element)
.setType(JSImplicitElement.Type.Class).setTypeString("A;" + elements + ";;");
elementBuilder.setUserString(ANGULAR_DIRECTIVES_INDEX_USER_STRING);
outData.addImplicitElement(elementBuilder.toImplicitElement());
}
return outData;
}
private static void addPipe(PsiElement expression, @NotNull JSElementIndexingDataImpl outData, String pipe) {
if (pipe == null) return;
JSImplicitElementImpl.Builder elementBuilder = new JSImplicitElementImpl.Builder(pipe, expression).setUserString(
ANGULAR_FILTER_INDEX_USER_STRING);
outData.addImplicitElement(elementBuilder.toImplicitElement());
}
private static boolean isTemplate(PsiElement decorator) {
final JSClass clazz = PsiTreeUtil.getParentOfType(decorator, JSClass.class);
if (clazz != null) {
final JSFunction constructor = clazz.getConstructor();
final JSParameterList params = constructor != null ? constructor.getParameterList() : null;
return params != null && params.getText().contains(TEMPLATE_REF);
}
final PsiElement parent = decorator.getParent();
if (parent instanceof JSArrayLiteralExpression) {
final JSCallExpression metadata = PsiTreeUtil.getNextSiblingOfType(decorator, JSCallExpression.class);
return hasTemplateRef(metadata);
}
if (parent instanceof JSObjectLiteralExpression) {
JSQualifiedName namespace = getCompiledDecoratorNamespace(parent);
if (namespace == null) return false;
final JSBlockStatement block = PsiTreeUtil.getParentOfType(parent, JSBlockStatement.class);
final JSFile file = block == null ? PsiTreeUtil.getParentOfType(parent, JSFile.class) : null;
final JSSourceElement[] statements = block != null ? block.getStatements() :
file != null ? file.getStatements() :
JSStatement.EMPTY;
for (JSSourceElement statement : statements) {
if (statement instanceof JSExpressionStatement) {
final JSExpression expression = ((JSExpressionStatement)statement).getExpression();
if (expression instanceof JSAssignmentExpression) {
final JSDefinitionExpression def = ((JSAssignmentExpression)expression).getDefinitionExpression();
if (def != null && "ctorParameters".equals(def.getName()) && namespace.equals(def.getJSNamespace().getQualifiedName())) {
return hasTemplateRef(expression) ||
PsiTreeUtil.hasErrorElements(expression) && !DialectDetector.isES6(expression) && hasTemplateRef(PsiTreeUtil.getNextSiblingOfType(statement, JSExpressionStatement.class));
}
}
}
}
}
return false;
}
private static boolean hasTemplateRef(@Nullable PsiElement expression) {
return expression != null && expression.getText().contains(TEMPLATE_REF);
}
private static JSQualifiedName getCompiledDecoratorNamespace(PsiElement parent) {
JSAssignmentExpression assignment = PsiTreeUtil.getParentOfType(parent, JSAssignmentExpression.class, true, JSFunction.class, JSFile.class);
JSDefinitionExpression definition = assignment != null ? assignment.getDefinitionExpression() : null;
return definition != null ? definition.getJSNamespace().getQualifiedName() : null;
}
@Nullable
private static String getPropertyName(PsiElement decorator, String name) {
final JSProperty selector = getProperty(decorator, name);
final JSExpression value = selector != null ? selector.getValue() : null;
if (value instanceof JSBinaryExpression) {
return JSInjectionController.getInjectionText(value);
}
if (value instanceof JSLiteralExpression && ((JSLiteralExpression)value).isQuotedLiteral()) {
return AngularJSIndexingHandler.unquote(value);
}
return null;
}
@Nullable
public static JSProperty getSelector(PsiElement decorator) {
return getProperty(decorator instanceof ES6Decorator ? PsiTreeUtil.findChildOfType(decorator, JSCallExpression.class) : decorator, SELECTOR);
}
@Nullable
private static JSProperty getProperty(PsiElement decorator, String name) {
final JSArgumentList argumentList = PsiTreeUtil.getChildOfType(decorator, JSArgumentList.class);
JSExpression[] arguments = argumentList != null ? argumentList.getArguments() : null;
if (arguments == null) {
final JSArrayLiteralExpression array = PsiTreeUtil.getChildOfType(decorator, JSArrayLiteralExpression.class);
arguments = array != null ? array.getExpressions() : null;
}
final JSObjectLiteralExpression descriptor = ObjectUtils.tryCast(arguments != null && arguments.length > 0 ? arguments[0] : null,
JSObjectLiteralExpression.class);
return descriptor != null ? descriptor.findProperty(name) : null;
}
public static boolean isDirective(@Nullable String name) {
return "Directive".equals(name) || "DirectiveAnnotation".equals(name) ||
"Component".equals(name) || "ComponentAnnotation".equals(name);
}
public static boolean isModule(@Nullable String name) {
return "NgModule".equals(name);
}
private static boolean isPipe(@Nullable String name) {
return "Pipe".equals(name);
}
@Override
public void addContextType(BaseJSSymbolProcessor.TypeInfo info, PsiElement context) {
if (context instanceof JSReferenceExpression && ((JSReferenceExpression)context).getQualifier() == null) {
final JSQualifiedName directiveNamespace = findDirective(context);
if (directiveNamespace != null) {
info.addType(new JSNamespaceImpl(directiveNamespace, JSContext.INSTANCE, true), false);
}
}
}
@Override
public void addContextNames(PsiElement context, List<String> names) {
if (context instanceof JSReferenceExpression && ((JSReferenceExpression)context).getQualifier() == null) {
final JSQualifiedName directiveNamespace = findDirective(context);
if (directiveNamespace != null) {
names.add(directiveNamespace.getQualifiedName());
}
}
}
@Nullable
private static JSQualifiedName findDirective(PsiElement context) {
JSClass clazz = findDirectiveClass(context);
return clazz != null ? JSQualifiedNameImpl.buildProvidedNamespace(clazz) : null;
}
@Nullable
public static JSClass findDirectiveClass(PsiElement context) {
final PsiFile file = context.getContainingFile();
if (file.getLanguage().is(Angular2HTMLLanguage.INSTANCE)) { // inline template
return PsiTreeUtil.getParentOfType(InjectedLanguageManager.getInstance(context.getProject()).getInjectionHost(file), JSClass.class);
}
if (file.getLanguage().is(AngularJSLanguage.INSTANCE)) { // template file with the same name
final PsiElement original = CompletionUtil.getOriginalOrSelf(context);
PsiFile hostFile = FileContextUtil.getContextFile(original != context ? original : context.getContainingFile().getOriginalFile());
final String name = hostFile != null ? hostFile.getVirtualFile().getNameWithoutExtension() : null;
final PsiDirectory dir = hostFile != null ? hostFile.getParent() : null;
final PsiFile directiveFile = dir != null ? dir.findFile(name + ".ts") : null;
if (directiveFile != null) {
for (PsiElement element : directiveFile.getChildren()) {
if (element instanceof JSClass) {
return (JSClass)element;
}
}
}
}
return null;
}
@Nullable
@Override
public JSLiteralImplicitElementCustomProvider createLiteralImplicitElementCustomProvider() {
return new JSLiteralImplicitElementCustomProvider() {
@Override
public boolean checkIfCandidate(@NotNull ASTNode literalExpression) {
ASTNode parent = TreeUtil.findParent(literalExpression, JSStubElementTypes.CALL_EXPRESSION);
LeafElement leaf = parent != null ? TreeUtil.findFirstLeaf(parent) : null;
return leaf != null && leaf.getText().startsWith(DECORATE);
}
@Override
public void fillIndexingDataForCandidate(@NotNull JSLiteralExpression argument, @NotNull JSElementIndexingData outIndexingData) {
String name = argument.isQuotedLiteral() ? AngularJSIndexingHandler.unquote(argument) : null;
if (name == null) return;
JSCallExpression callExpression = PsiTreeUtil.getParentOfType(argument, JSCallExpression.class);
if (callExpression == null) return;
JSExpression first = callExpression.getArguments()[0];
if (!(first instanceof JSArrayLiteralExpression)) return;
JSExpression[] expressions = ((JSArrayLiteralExpression)first).getExpressions();
if (expressions.length != 2) return;
JSExpression decorator = expressions[0];
String decoratorName = getCallName(decorator);
if (!"Input".equals(decoratorName) && !"Output".equals(decoratorName)) return;
JSExpression metadata = expressions[1];
String metadataName = getCallName(metadata);
if (metadataName == null || !metadataName.startsWith("__metadata")) return;
JSExpression[] meta = ((JSCallExpression)metadata).getArguments();
if (meta.length != 2) return;
if (!(meta[0] instanceof JSLiteralExpression)) return;
String type = AngularJSIndexingHandler.unquote(meta[0]);
if (!"design:type".equals(type)) return;
JSImplicitElementImpl.Builder builder =
new JSImplicitElementImpl.Builder(getDecoratedName(name, decorator), argument).setUserString(DECORATORS)
.setTypeString(decoratorName + ";" + meta[1].getText());
outIndexingData.addImplicitElement(builder.toImplicitElement());
}
private String getDecoratedName(String name, JSExpression decorator) {
if (decorator instanceof JSCallExpression) {
final JSExpression expression = ((JSCallExpression)decorator).getMethodExpression();
if (expression instanceof JSReferenceExpression) {
JSExpression[] arguments = ((JSCallExpression)decorator).getArguments();
if (arguments.length > 0 && arguments[0] instanceof JSLiteralExpression) {
Object value = ((JSLiteralExpression)arguments[0]).getValue();
if (value instanceof String) return (String)value;
}
}
}
return name;
}
private String getCallName(JSExpression call) {
if (call instanceof JSCallExpression) {
JSExpression expression = ((JSCallExpression)call).getMethodExpression();
if (expression instanceof JSReferenceExpression) {
return ((JSReferenceExpression)expression).getReferenceName();
}
}
return null;
}
};
}
@Override
public boolean indexImplicitElement(@NotNull JSImplicitElementStructure element, @Nullable IndexSink sink) {
if (sink != null && DECORATORS.equals(element.getUserString())) {
sink.occurrence(AngularDecoratorsIndex.KEY, element.getName());
sink.occurrence(AngularSymbolIndex.KEY, element.getName());
return true;
}
return false;
}
@Override
public int getVersion() {
return AngularIndexUtil.BASE_VERSION;
}
}