package org.angularjs.index; import com.intellij.lang.ASTNode; import com.intellij.lang.javascript.JSDocTokenTypes; import com.intellij.lang.javascript.documentation.JSDocumentationUtils; import com.intellij.lang.javascript.index.FrameworkIndexingHandler; import com.intellij.lang.javascript.index.JSSymbolUtil; import com.intellij.lang.javascript.library.JSLibraryUtil; import com.intellij.lang.javascript.psi.*; import com.intellij.lang.javascript.psi.impl.JSCallExpressionImpl; import com.intellij.lang.javascript.psi.impl.JSPsiImplUtils; import com.intellij.lang.javascript.psi.impl.JSReferenceExpressionImpl; import com.intellij.lang.javascript.psi.jsdoc.JSDocComment; import com.intellij.lang.javascript.psi.jsdoc.JSDocTag; import com.intellij.lang.javascript.psi.jsdoc.JSDocTagValue; import com.intellij.lang.javascript.psi.literal.JSLiteralImplicitElementProvider; import com.intellij.lang.javascript.psi.resolve.JSTypeEvaluator; 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.lang.javascript.psi.types.JSNamedType; import com.intellij.lang.javascript.psi.types.JSTypeSource; import com.intellij.lang.javascript.psi.types.JSTypeSourceFactory; import com.intellij.lang.javascript.psi.util.JSTreeUtil; import com.intellij.openapi.util.Pair; import com.intellij.openapi.util.Ref; import com.intellij.openapi.util.text.StringUtil; import com.intellij.openapi.vfs.VirtualFile; import com.intellij.psi.FileViewProvider; import com.intellij.psi.PsiElement; import com.intellij.psi.PsiNamedElement; import com.intellij.psi.PsiWhiteSpace; import com.intellij.psi.impl.source.tree.CompositeElement; import com.intellij.psi.stubs.IndexSink; import com.intellij.psi.stubs.StubIndexKey; import com.intellij.psi.util.PsiTreeUtil; import com.intellij.testFramework.LightVirtualFile; import com.intellij.util.*; import com.intellij.util.containers.BidirectionalMap; import gnu.trove.THashSet; import org.angularjs.codeInsight.DirectiveUtil; import org.angularjs.codeInsight.router.AngularJSUiRouterConstants; import org.angularjs.lang.AngularJSLanguage; import org.angularjs.lang.psi.AngularJSAsExpression; import org.angularjs.lang.psi.AngularJSFilterExpression; import org.angularjs.lang.psi.AngularJSRepeatExpression; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import java.util.*; /** * @author Dennis.Ushakov */ public class AngularJSIndexingHandler extends FrameworkIndexingHandler { private static final Map<String, StubIndexKey<String, JSImplicitElementProvider>> INDEXERS = new HashMap<>(); private static final Map<String, Function<String, String>> NAME_CONVERTERS = new HashMap<>(); private static final Map<String, Function<PsiElement, String>> DATA_CALCULATORS = new HashMap<>(); private static final Map<String, PairProcessor<JSProperty, JSElementIndexingData>> CUSTOM_PROPERTY_PROCESSORS = new HashMap<>(); private final static Map<String, Function<String, List<String>>> POLY_NAME_CONVERTERS = new HashMap<>(); private final static Map<String, Processor<JSArgumentList>> ARGUMENT_LIST_CHECKERS = new HashMap<>(); public static final Set<String> INTERESTING_METHODS = new HashSet<>(); public static final Set<String> INJECTABLE_METHODS = new HashSet<>(); public static final String CONTROLLER = "controller"; public static final String DIRECTIVE = "directive"; public static final String COMPONENT = "component"; public static final String BINDINGS = "bindings"; public static final String MODULE = "module"; public static final String FILTER = "filter"; public static final String STATE = "state"; private static final String START_SYMBOL = "startSymbol"; private static final String END_SYMBOL = "endSymbol"; public static final String DEFAULT_RESTRICTIONS = "D"; public static final String WHEN = "when"; private static final String[] ALL_INTERESTING_METHODS; private static final BidirectionalMap<String, StubIndexKey<String, JSImplicitElementProvider>> INDEXES; public static final String AS_CONNECTOR_WITH_SPACES = " as "; public static final String ANGULAR_DIRECTIVES_INDEX_USER_STRING = "adi"; public static final String ANGULAR_FILTER_INDEX_USER_STRING = "afi"; public static final String ANGULAR_SYMBOL_INDEX_USER_STRING = "asi"; public static final String ANGULAR_MODULE_INDEX_USER_STRING = "ami"; static { Collections.addAll(INTERESTING_METHODS, "service", "factory", "value", "constant", "provider"); INJECTABLE_METHODS.addAll(INTERESTING_METHODS); Collections.addAll(INJECTABLE_METHODS, CONTROLLER, DIRECTIVE, COMPONENT, MODULE, "config", "run"); INDEXERS.put(DIRECTIVE, AngularDirectivesIndex.KEY); NAME_CONVERTERS.put(DIRECTIVE, DirectiveUtil::getAttributeName); DATA_CALCULATORS.put(DIRECTIVE, element -> calculateRestrictions(element, DEFAULT_RESTRICTIONS)); INDEXERS.put(COMPONENT, AngularDirectivesIndex.KEY); NAME_CONVERTERS.put(COMPONENT, NAME_CONVERTERS.get(DIRECTIVE)); DATA_CALCULATORS.put(COMPONENT, element -> calculateRestrictions(element, "E")); INDEXERS.put(CONTROLLER, AngularControllerIndex.KEY); INDEXERS.put(MODULE, AngularModuleIndex.KEY); INDEXERS.put(FILTER, AngularFilterIndex.KEY); INDEXERS.put(STATE, AngularUiRouterStatesIndex.KEY); final THashSet<String> allInterestingMethods = new THashSet<>(INTERESTING_METHODS); allInterestingMethods.addAll(INJECTABLE_METHODS); allInterestingMethods.addAll(INDEXERS.keySet()); allInterestingMethods.add(START_SYMBOL); allInterestingMethods.add(END_SYMBOL); ALL_INTERESTING_METHODS = ArrayUtil.toStringArray(allInterestingMethods); INDEXES = new BidirectionalMap<>(); INDEXES.put("aci", AngularControllerIndex.KEY); INDEXES.put("addi", AngularDirectivesDocIndex.KEY); INDEXES.put(ANGULAR_DIRECTIVES_INDEX_USER_STRING, AngularDirectivesIndex.KEY); INDEXES.put(ANGULAR_FILTER_INDEX_USER_STRING, AngularFilterIndex.KEY); INDEXES.put("aidi", AngularInjectionDelimiterIndex.KEY); INDEXES.put(ANGULAR_MODULE_INDEX_USER_STRING, AngularModuleIndex.KEY); INDEXES.put(ANGULAR_SYMBOL_INDEX_USER_STRING, AngularSymbolIndex.KEY); INDEXES.put("arsi", AngularUiRouterStatesIndex.KEY); INDEXES.put("arsgi", AngularUiRouterGenericStatesIndex.KEY); INDEXES.put("agmi", AngularGenericModulesIndex.KEY); for (String key : INDEXES.keySet()) { JSImplicitElement.ourUserStringsRegistry.registerUserString(key); } CUSTOM_PROPERTY_PROCESSORS.put(COMPONENT, AngularJSIndexingHandler::bindingsProcessor); NAME_CONVERTERS.put(BINDINGS, NAME_CONVERTERS.get(DIRECTIVE)); final PairProcessor<JSProperty, JSElementIndexingData> processor = createRouterParametersProcessor(); CUSTOM_PROPERTY_PROCESSORS.put(WHEN, processor); CUSTOM_PROPERTY_PROCESSORS.put("otherwise", processor); CUSTOM_PROPERTY_PROCESSORS.put("state", processor); // example of nested states https://scotch.io/tutorials/angular-routing-using-ui-router POLY_NAME_CONVERTERS.put(STATE, (NotNullFunction<String, List<String>>)dom -> { final String[] parts = dom.split("\\."); final List<String> result = new ArrayList<>(); result.add(dom); String tail = ""; for (int i = parts.length - 1; i > 0; i--) { final String part = "." + parts[i] + tail; result.add(part); tail = part; } return result; }); // do NOT split module names by dot POLY_NAME_CONVERTERS.put(MODULE, Collections::singletonList); ARGUMENT_LIST_CHECKERS.put(MODULE, list -> list.getArguments().length > 1); } static final String RESTRICT = "@restrict"; static final String ELEMENT = "@element"; private static final String PARAM = "@param"; public static boolean isInjectable(PsiElement context) { final JSCallExpression call = PsiTreeUtil.getParentOfType(context, JSCallExpression.class, false, JSBlockStatement.class); if (call != null) { final JSExpression methodExpression = call.getMethodExpression(); JSReferenceExpression callee = ObjectUtils.tryCast(methodExpression, JSReferenceExpression.class); JSExpression qualifier = callee != null ? callee.getQualifier() : null; return qualifier != null && INJECTABLE_METHODS.contains(callee.getReferenceName()); } return false; } @NotNull @Override public String[] interestedMethodNames() { return ALL_INTERESTING_METHODS; } @Override public JSLiteralImplicitElementProvider createLiteralImplicitElementProvider(@NotNull final String command) { return new JSLiteralImplicitElementProvider() { @Override public void fillIndexingData(@NotNull JSLiteralExpression argument, @NotNull JSCallExpression callExpression, @NotNull JSElementIndexingData outIndexingData) { JSExpression[] arguments = callExpression.getArguments(); if (arguments.length == 0 || arguments[0] != argument) return; final JSExpression methodExpression = callExpression.getMethodExpression(); if (!(methodExpression instanceof JSReferenceExpression)) return; JSExpression qualifier = ((JSReferenceExpression)methodExpression).getQualifier(); if (qualifier == null) return; final StubIndexKey<String, JSImplicitElementProvider> index = INDEXERS.get(command); if (index != null) { if (argument.isQuotedLiteral()) { final Processor<JSArgumentList> argumentListProcessor = ARGUMENT_LIST_CHECKERS.get(command); if (argumentListProcessor != null && !argumentListProcessor.process(callExpression.getArgumentList())) return; final Function<PsiElement, String> calculator = DATA_CALCULATORS.get(command); final String data = calculator != null ? calculator.fun(argument) : null; final String argumentText = unquote(argument); addImplicitElements(argument, command, index, argumentText, data, outIndexingData); } } else if (INJECTABLE_METHODS.contains(command)) { // INTERESTING_METHODS are contained in INJECTABLE_METHODS if (argument.isQuotedLiteral()) { generateNamespace(argument, outIndexingData); } } if (START_SYMBOL.equals(command) || END_SYMBOL.equals(command)) { while (qualifier != null) { if (qualifier instanceof JSReferenceExpression) { if ("$interpolateProvider".equals(((JSReferenceExpression)qualifier).getReferenceName())) { if (argument.isQuotedLiteral()) { String interpolation = unquote(argument); // '//' interpolations are usually dragged from examples folder and not supposed to be used by real users if ("//".equals(interpolation)) return; FileViewProvider provider = qualifier.getContainingFile().getOriginalFile().getViewProvider(); VirtualFile virtualFile = provider.getVirtualFile(); virtualFile = virtualFile instanceof LightVirtualFile ? ((LightVirtualFile)virtualFile).getOriginalFile() : virtualFile; if (JSLibraryUtil.isProbableLibraryFile(virtualFile)) return; addImplicitElements(argument, null, AngularInjectionDelimiterIndex.KEY, command, interpolation, outIndexingData); } } qualifier = ((JSReferenceExpression)qualifier).getQualifier(); } else { qualifier = qualifier instanceof JSCallExpression ? ((JSCallExpression)qualifier).getMethodExpression() : null; } } } } }; } @Override public void processCallExpression(JSCallExpression callExpression, @NotNull JSElementIndexingData outData) { final JSReferenceExpression reference = ObjectUtils.tryCast(callExpression.getMethodExpression(), JSReferenceExpression.class); if (reference == null) return; if (JSSymbolUtil.isAccurateReferenceExpressionName(reference, "$stateProvider", STATE)) { final JSExpression[] arguments = callExpression.getArguments(); if (arguments.length == 1 && arguments[0] instanceof JSReferenceExpression) { addImplicitElements(callExpression, null, AngularUiRouterGenericStatesIndex.KEY, STATE, null, outData); } } else if (JSSymbolUtil.isAccurateReferenceExpressionName(reference, "angular", MODULE)) { final JSExpression[] arguments = callExpression.getArguments(); if (arguments.length > 1 && arguments[0] instanceof JSReferenceExpression) { addImplicitElements(callExpression, null, AngularGenericModulesIndex.KEY, MODULE, null, outData); } } } @Override public boolean shouldCreateStubForCallExpression(ASTNode node) { final ASTNode methodExpression = JSCallExpressionImpl.getMethodExpression(node); if (methodExpression == null) return false; final ASTNode referencedNameElement = methodExpression.getLastChildNode(); final ASTNode qualifier = JSReferenceExpressionImpl.getQualifierNode(methodExpression); if (qualifier == null) return false; return STATE.equals(referencedNameElement.getText()) && "$stateProvider".equalsIgnoreCase(qualifier.getText()) || MODULE.equals(referencedNameElement.getText()) && "angular".equalsIgnoreCase(qualifier.getText()); } @Nullable @Override public JSElementIndexingData processAnyProperty(@NotNull JSProperty property, @Nullable JSElementIndexingData outData) { final String name = property.getName(); if (name == null) return outData; final Pair<JSCallExpression, Integer> pair = findImmediatelyWrappingCall(property); if (pair == null) return outData; final JSCallExpression callExpression = pair.getFirst(); final int level = pair.getSecond(); final JSExpression methodExpression = callExpression.getMethodExpression(); if (!(methodExpression instanceof JSReferenceExpression) || ((JSReferenceExpression)methodExpression).getQualifier() == null) { return outData; } final String command = ((JSReferenceExpression)methodExpression).getReferenceName(); final PairProcessor<JSProperty, JSElementIndexingData> customProcessor = CUSTOM_PROPERTY_PROCESSORS.get(command); JSElementIndexingData localOutData; if (customProcessor != null && customProcessor.process(property, (localOutData = (outData == null ? new JSElementIndexingDataImpl() : outData)))) { return localOutData; } // for 'standard' properties, keep indexing only for properties - immediate children of function calls parameters if (level > 1) return outData; final PsiElement parent = property.getParent(); final StubIndexKey<String, JSImplicitElementProvider> index = INDEXERS.get(command); if (index == null) return outData; if (callExpression.getArguments()[0] != parent) return outData; if (outData == null) outData = new JSElementIndexingDataImpl(); addImplicitElements(property, command, index, name, null, outData); return outData; } @Nullable private static Pair<JSCallExpression, Integer> findImmediatelyWrappingCall(@NotNull JSProperty property) { PsiElement current = property.getParent(); int level = 0; while (current != null && current instanceof JSElement) { if (current instanceof JSProperty) { current = current.getParent(); continue; } else if (current instanceof JSObjectLiteralExpression) { ++level; current = current.getParent(); continue; } if (current instanceof JSArgumentList) { final PsiElement callExpression = current.getParent(); if (callExpression instanceof JSCallExpression) return Pair.create((JSCallExpression)callExpression, level); } return null; } return null; } @Override public boolean indexImplicitElement(@NotNull JSImplicitElementStructure element, @Nullable IndexSink sink) { final String userID = element.getUserString(); final StubIndexKey<String, JSImplicitElementProvider> index = userID != null ? INDEXES.get(userID) : null; if (index != null) { if (sink != null) { sink.occurrence(index, element.getName()); if (index != AngularSymbolIndex.KEY) { sink.occurrence(AngularSymbolIndex.KEY, element.getName()); } } } return false; } @Override public JSElementIndexingData processJSDocComment(@NotNull final JSDocComment comment, @Nullable JSElementIndexingData outData) { JSDocTag ngdocTag = null; JSDocTag nameTag = null; for (JSDocTag tag : comment.getTags()) { if ("ngdoc".equals(tag.getName())) ngdocTag = tag; else if ("name".equals(tag.getName())) nameTag = tag; } if (ngdocTag != null && nameTag != null) { final JSDocTagValue nameValue = nameTag.getValue(); String name = nameValue != null ? nameValue.getText() : null; if (name != null) name = name.substring(name.indexOf(':') + 1); String ngdocValue = null; PsiElement nextSibling = ngdocTag.getNextSibling(); if (nextSibling instanceof PsiWhiteSpace) nextSibling = nextSibling.getNextSibling(); if (nextSibling != null && nextSibling.getNode().getElementType() == JSDocTokenTypes.DOC_COMMENT_DATA) { ngdocValue = nextSibling.getText(); } if (ngdocValue != null && name != null) { final String[] commentLines = StringUtil.splitByLines(comment.getText()); final boolean directive = ngdocValue.contains(DIRECTIVE); final boolean component = ngdocValue.contains(COMPONENT); if (directive || component) { final String restrictions = calculateRestrictions(commentLines, directive ? DEFAULT_RESTRICTIONS : "E"); if (outData == null) outData = new JSElementIndexingDataImpl(); addImplicitElements(comment, directive ? DIRECTIVE : COMPONENT, AngularDirectivesDocIndex.KEY, name, restrictions, outData); } else if (ngdocValue.contains(FILTER)) { if (outData == null) outData = new JSElementIndexingDataImpl(); addImplicitElements(comment, FILTER, AngularFilterIndex.KEY, name, null, outData); } } } return outData; } private static String calculateRestrictions(final String[] commentLines, String defaultRestrictions) { String restrict = defaultRestrictions; String tag = ""; String param = ""; StringBuilder attributes = new StringBuilder(); for (String line : commentLines) { restrict = getParamValue(restrict, line, RESTRICT); tag = getParamValue(tag, line, ELEMENT); final int start = line.indexOf(PARAM); if (start >= 0) { final JSDocumentationUtils.DocTag docTag = JSDocumentationUtils.getDocTag(line.substring(start)); if (docTag != null) { param = docTag.matchValue != null ? docTag.matchValue : param; if (attributes.length() > 0) attributes.append(","); attributes.append(docTag.matchName); } } } return restrict + ";" + tag + ";" + param.trim() + ";" + attributes.toString().trim(); } public static boolean isAngularRestrictions(@Nullable String restrictions) { return restrictions == null || StringUtil.countChars(restrictions, ';') >= 3; } static String getParamValue(String previousValue, String line, final String docTag) { final int indexOfTag = line.indexOf(docTag); if (indexOfTag >= 0) { final int commentAtEndIndex = line.indexOf("//", indexOfTag); String newValue = line.substring(indexOfTag + docTag.length(), commentAtEndIndex > 0 ? commentAtEndIndex : line.length()); newValue = newValue.trim(); if (!StringUtil.isEmpty(newValue)) return newValue; } return previousValue; } private static void generateNamespace(@NotNull JSLiteralExpression argument, @NotNull JSElementIndexingData outData) { final String namespace = unquote(argument); if (namespace == null) return; JSQualifiedNameImpl qName = JSQualifiedNameImpl.fromQualifiedName(namespace); JSImplicitElementImpl.Builder elementBuilder = new JSImplicitElementImpl.Builder(qName, argument) .setType(JSImplicitElement.Type.Class).setUserString(ANGULAR_SYMBOL_INDEX_USER_STRING); final JSImplicitElementImpl implicitElement = elementBuilder.toImplicitElement(); outData.addImplicitElement(implicitElement); // TODO fix //final JSFunction function = findFunction(argument); //final JSNamespace ns = visitor.findNsForExpr((JSExpression)argument); //if (function != null && ns != null) { // visitor.visitWithNamespace(ns, function, false); //} } private static void addImplicitElements(@NotNull final JSImplicitElementProvider elementProvider, @Nullable final String command, @NotNull final StubIndexKey<String, JSImplicitElementProvider> index, @Nullable String defaultName, @Nullable final String value, @NotNull final JSElementIndexingData outData) { if (defaultName == null) return; final List<String> keys = INDEXES.getKeysByValue(index); assert keys != null && keys.size() == 1; final Consumer<JSImplicitElementImpl.Builder> adder = builder -> { builder.setType(elementProvider instanceof JSDocComment ? JSImplicitElement.Type.Tag : JSImplicitElement.Type.Class) .setTypeString(value); builder.setUserString(keys.get(0)); final JSImplicitElementImpl implicitElement = builder.toImplicitElement(); outData.addImplicitElement(implicitElement); }; final Function<String, List<String>> variants = POLY_NAME_CONVERTERS.get(command); final Function<String, String> converter = command != null ? NAME_CONVERTERS.get(command) : null; final String name = converter != null ? converter.fun(defaultName) : defaultName; if (variants != null) { final List<String> strings = variants.fun(name); for (String string : strings) { adder.consume(new JSImplicitElementImpl.Builder(string, elementProvider)); } } else { adder.consume(new JSImplicitElementImpl.Builder(JSQualifiedNameImpl.fromQualifiedName(name), elementProvider)); } if (!StringUtil.equals(defaultName, name)) { JSImplicitElementImpl.Builder symbolElementBuilder = new JSImplicitElementImpl.Builder(defaultName, elementProvider) .setType(elementProvider instanceof JSDocComment ? JSImplicitElement.Type.Tag : JSImplicitElement.Type.Class) .setTypeString(value); final List<String> symbolKeys = INDEXES.getKeysByValue(AngularSymbolIndex.KEY); assert symbolKeys != null && symbolKeys.size() == 1; symbolElementBuilder.setUserString(symbolKeys.get(0)); final JSImplicitElementImpl implicitElement2 = symbolElementBuilder.toImplicitElement(); outData.addImplicitElement(implicitElement2); } } private static String calculateRestrictions(PsiElement element, String defaultRestrictions) { final Ref<String> restrict = Ref.create(defaultRestrictions); final Ref<String> scope = Ref.create(""); final PsiElement function = findFunction(element); if (function != null) { function.accept(new JSRecursiveElementVisitor() { @Override public void visitJSProperty(JSProperty node) { final String name = node.getName(); final JSExpression value = node.getValue(); if ("restrict".equals(name)) { if (value instanceof JSLiteralExpression && ((JSLiteralExpression)value).isQuotedLiteral()) { final String unquoted = unquote(value); if (unquoted != null) restrict.set(unquoted); } } else if ("scope".equals(name)) { if (value instanceof JSObjectLiteralExpression) { scope.set(StringUtil.join(((JSObjectLiteralExpression)value).getProperties(), PsiNamedElement::getName, ",")); } } } }); } return restrict.get().trim() + ";;;" + scope.get(); } private static PsiElement findFunction(PsiElement element) { PsiElement function = PsiTreeUtil.getNextSiblingOfType(element, JSFunction.class); if (function == null) { function = PsiTreeUtil.getNextSiblingOfType(element, JSObjectLiteralExpression.class); } if (function == null) { final JSExpression expression = PsiTreeUtil.getNextSiblingOfType(element, JSExpression.class); function = findDeclaredFunction(expression); } if (function == null) { JSArrayLiteralExpression array = PsiTreeUtil.getNextSiblingOfType(element, JSArrayLiteralExpression.class); function = PsiTreeUtil.findChildOfType(array, JSFunction.class); if (function == null) { final JSExpression candidate = array != null ? PsiTreeUtil.getPrevSiblingOfType(array.getLastChild(), JSExpression.class) : null; function = findDeclaredFunction(candidate); } } return function; } private static PsiElement findDeclaredFunction(JSExpression expression) { final String name = expression instanceof JSReferenceExpression ? ((JSReferenceExpression)expression).getReferenceName() : null; if (name != null) { ASTNode node = expression.getNode(); final JSTreeUtil.JSScopeDeclarationsAndAssignments declaration = JSTreeUtil.getDeclarationsAndAssignmentsInScopeAndUp(name, node); CompositeElement definition = declaration != null ? declaration.findNearestDefinition(node) : null; if (definition != null) { return definition.getPsi(); } } return null; } @Override public String resolveContextFromProperty(JSObjectLiteralExpression objectLiteralExpression, boolean returnPropertiesNamespace) { if (!(objectLiteralExpression.getParent() instanceof JSReturnStatement)) return null; final JSFunction function = PsiTreeUtil.getParentOfType(objectLiteralExpression, JSFunction.class); final JSCallExpression call = PsiTreeUtil.getParentOfType(function, JSCallExpression.class); if (call != null) { final JSExpression methodExpression = call.getMethodExpression(); if (!(methodExpression instanceof JSReferenceExpression)) return null; JSReferenceExpression callee = (JSReferenceExpression)methodExpression; JSExpression qualifier = callee.getQualifier(); if (qualifier == null) return null; final String command = callee.getReferencedName(); if (INJECTABLE_METHODS.contains(command)) { JSExpression[] arguments = call.getArguments(); if (arguments.length > 0) { JSExpression argument = arguments[0]; if (argument instanceof JSLiteralExpression && ((JSLiteralExpression)argument).isQuotedLiteral()) { return unquote(argument); } } } } return null; } @Override public boolean addTypeFromResolveResult(JSTypeEvaluator evaluator, PsiElement resolveResult, boolean hasSomeType) { if (!AngularIndexUtil.hasAngularJS(resolveResult.getProject())) return false; if (resolveResult instanceof JSDefinitionExpression && resolveResult.getLanguage() instanceof AngularJSLanguage) { final PsiElement resolveParent = resolveResult.getParent(); if (resolveParent instanceof AngularJSAsExpression) { final String name = resolveParent.getFirstChild().getText(); final JSTypeSource source = JSTypeSourceFactory.createTypeSource(resolveResult); final JSType type = JSNamedType.createType(name, source, JSContext.INSTANCE); evaluator.addType(type, resolveResult); return true; } } if (resolveResult instanceof JSVariable && resolveResult.getLanguage() instanceof AngularJSLanguage) { PsiElement resolveParent = resolveResult.getParent().getParent(); if (resolveParent instanceof AngularJSRepeatExpression) { if (calculateRepeatParameterType(evaluator, (AngularJSRepeatExpression)resolveParent)) { return true; } } } if (resolveResult instanceof JSParameter && evaluator.isFromCurrentFile(resolveResult) && isInjectable(resolveResult)) { final String name = ((JSParameter)resolveResult).getName(); final JSTypeSource source = JSTypeSourceFactory.createTypeSource(resolveResult); final JSType type = JSNamedType.createType(name, source, JSContext.UNKNOWN); evaluator.addType(type, resolveResult); } return false; } @Override public int getVersion() { return AngularIndexUtil.BASE_VERSION; } private static boolean calculateRepeatParameterType(JSTypeEvaluator evaluator, AngularJSRepeatExpression resolveParent) { final PsiElement last = findReferenceExpression(resolveParent); JSExpression arrayExpression = null; if (last instanceof JSReferenceExpression) { PsiElement resolve = ((JSReferenceExpression)last).resolve(); if (resolve != null) { if (resolve instanceof JSTypeDeclarationOwner) { JSType type = ((JSTypeDeclarationOwner)resolve).getType(); if (type != null) { return evaluator.addComponentTypeFromProcessor((JSExpression)last, type) != null; } } resolve = JSPsiImplUtils.getAssignedExpression(resolve); if (resolve != null) { arrayExpression = (JSExpression)resolve; } } } else if (last instanceof JSExpression) { arrayExpression = (JSExpression)last; } if (last != null && arrayExpression != null) { return evaluator.evalComponentTypeFromArrayExpression(resolveParent, arrayExpression) != null; } return false; } private static PsiElement findReferenceExpression(AngularJSRepeatExpression parent) { JSExpression collection = parent.getCollection(); while (collection instanceof JSBinaryExpression && ((JSBinaryExpression)collection).getROperand() instanceof AngularJSFilterExpression) { collection = ((JSBinaryExpression)collection).getLOperand(); } return collection; } private static PairProcessor<JSProperty, JSElementIndexingData> createRouterParametersProcessor() { return new PairProcessor<JSProperty, JSElementIndexingData>() { @Override public boolean process(JSProperty property, JSElementIndexingData outData) { if (!(property.getValue() instanceof JSLiteralExpression)) return true; final JSLiteralExpression value = (JSLiteralExpression)property.getValue(); if (!value.isQuotedLiteral()) return true; final String unquotedValue = unquote(value); if (AngularJSUiRouterConstants.controllerAs.equals(property.getName())) { return recordControllerAs(property, outData, value, unquotedValue); } else if (CONTROLLER.equals(property.getName())) { final int idx = unquotedValue != null ? unquotedValue.indexOf(AS_CONNECTOR_WITH_SPACES) : 0; if (idx > 0 && (idx + AS_CONNECTOR_WITH_SPACES.length()) < (unquotedValue.length() - 1)) { return recordControllerAs(property, outData, value, unquotedValue.substring(idx + AS_CONNECTOR_WITH_SPACES.length(), unquotedValue.length())); } } else if ("name".equals(property.getName())) { addImplicitElements(value, STATE, INDEXERS.get(STATE), unquotedValue, null, outData); return true; } return true; } private boolean recordControllerAs(JSProperty property, JSElementIndexingData outData, JSLiteralExpression value, String unquotedValue) { final StubIndexKey<String, JSImplicitElementProvider> index = INDEXERS.get(CONTROLLER); assert index != null; final JSObjectLiteralExpression object = ObjectUtils.tryCast(property.getParent(), JSObjectLiteralExpression.class); if (object == null) return false; final JSProperty controllerProperty = object.findProperty(CONTROLLER); if (controllerProperty != null) { // value (JSFunctionExpression) is not implicit element provider addImplicitElements(controllerProperty, null, index, unquotedValue, null, outData); return true; } addImplicitElements(value, null, index, unquotedValue, null, outData); return true; } }; } @Nullable static String unquote(PsiElement value) { return (String)((JSLiteralExpression)value).getValue(); } private static boolean bindingsProcessor(JSProperty property, JSElementIndexingData data) { PsiElement parent = property.getParent(); if (parent instanceof JSObjectLiteralExpression && parent.getParent() instanceof JSProperty && BINDINGS.equals(((JSProperty)parent.getParent()).getName())) { Pair<JSCallExpression, Integer> call = findImmediatelyWrappingCall(property); assert call != null; JSExpression[] arguments = call.first.getArguments(); if (arguments.length < 2 || !(arguments[0] instanceof JSLiteralExpression) || !((JSLiteralExpression)arguments[0]).isQuotedLiteral()) return false; final String componentName = unquote(arguments[0]); addImplicitElements(property, BINDINGS, AngularDirectivesDocIndex.KEY, DirectiveUtil.getAttributeName(property.getName()), "A;" + DirectiveUtil.getAttributeName(componentName) + ";expression;", data); return true; } return false; } }