package org.angularjs.codeInsight.refs;
import com.intellij.lang.javascript.index.JSSymbolUtil;
import com.intellij.lang.javascript.inspections.JSUnusedGlobalSymbolsInspection;
import com.intellij.lang.javascript.psi.*;
import com.intellij.lang.javascript.psi.resolve.JSResolveResult;
import com.intellij.lang.javascript.psi.stubs.JSImplicitElement;
import com.intellij.lang.javascript.refactoring.JSDefaultRenameProcessor;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.Pair;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.psi.*;
import com.intellij.psi.search.GlobalSearchScope;
import com.intellij.psi.search.LocalSearchScope;
import com.intellij.psi.search.PsiSearchHelper;
import com.intellij.psi.search.SearchScope;
import com.intellij.psi.util.CachedValueProvider;
import com.intellij.psi.util.CachedValuesManager;
import com.intellij.psi.util.PsiTreeUtil;
import com.intellij.util.ArrayUtil;
import com.intellij.util.CommonProcessors;
import com.intellij.util.ProcessingContext;
import com.intellij.util.containers.ContainerUtil;
import org.angularjs.index.AngularGenericModulesIndex;
import org.angularjs.index.AngularIndexUtil;
import org.angularjs.index.AngularJSIndexingHandler;
import org.angularjs.index.AngularModuleIndex;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.*;
/**
* @author Irina.Chernushina on 3/22/2016.
*/
public class AngularJSModuleReferencesProvider extends PsiReferenceProvider {
public static final String ANGULAR = "angular";
@NotNull
@Override
public PsiReference[] getReferencesByElement(@NotNull PsiElement element, @NotNull ProcessingContext context) {
return new PsiReference[] {new AngularJSModuleReference((JSLiteralExpression)element)};
}
private static class AngularJSModuleReference extends AngularPolyReferenceBase<JSLiteralExpression> {
public AngularJSModuleReference(JSLiteralExpression element) {
super(element, ElementManipulators.getValueTextRange(element));
}
private String getModuleName() {
return StringUtil.unquoteString(getCanonicalText());
}
@NotNull
@Override
protected ResolveResult[] resolveInner() {
if(! isAngularModuleReferenceAccurate()) return ResolveResult.EMPTY_ARRAY;
final String moduleName = getModuleName();
if (StringUtil.isEmptyOrSpaces(moduleName)) return ResolveResult.EMPTY_ARRAY;
final CommonProcessors.CollectProcessor<JSImplicitElement> collectProcessor = new CommonProcessors.CollectProcessor<>();
AngularIndexUtil.multiResolve(getElement().getProject(), AngularModuleIndex.KEY, moduleName, collectProcessor);
final Collection<JSImplicitElement> results = collectProcessor.getResults();
if (results.isEmpty()) return getGenericResolvedModules(moduleName);
final List<ResolveResult> resolveResults = ContainerUtil.map(results, AngularIndexUtil.JS_IMPLICIT_TO_RESOLVE_RESULT);
return resolveResults.toArray(ResolveResult.EMPTY_ARRAY);
}
private boolean isAngularModuleReferenceAccurate() {
final PsiElement parent = myElement.getParent();
if (parent instanceof JSArgumentList && parent.getParent() instanceof JSCallExpression
&& ((JSArgumentList)parent).getArguments().length == 1) {
if (PsiTreeUtil.isAncestor(((JSArgumentList)parent).getArguments()[0], myElement, false)) {
final JSExpression methodExpression = ((JSCallExpression)parent.getParent()).getMethodExpression();
if (methodExpression instanceof JSReferenceExpression &&
JSSymbolUtil.isAccurateReferenceExpressionName((JSReferenceExpression)methodExpression, ANGULAR, AngularJSIndexingHandler.MODULE)) {
return true;
}
if (AngularJSReferencesContributor.looksLikeAngularModuleReference(methodExpression)) {
//noinspection ConstantConditions
final JSExpression qualifier = ((JSReferenceExpression)methodExpression).getQualifier();
if (qualifier instanceof JSReferenceExpression) {
final PsiElement resolve = ((JSReferenceExpression)qualifier).resolve();
if (resolve instanceof JSVariable && ((JSVariable)resolve).getInitializer() instanceof JSReferenceExpression &&
JSSymbolUtil.isAccurateReferenceExpressionName((JSReferenceExpression) ((JSVariable)resolve).getInitializer(), ANGULAR)) {
return true;
}
}
}
}
}
return false;
}
private ResolveResult[] getGenericResolvedModules(String moduleName) {
final Project project = myElement.getProject();
final Collection<String> allKeys = AngularIndexUtil.getAllKeys(AngularGenericModulesIndex.KEY, project);
final List<JSImplicitElement> list = new ArrayList<>();
for (String key : allKeys) {
AngularIndexUtil.multiResolve(project, AngularGenericModulesIndex.KEY, key, list::add);
}
final List<ResolveResult> result = new ArrayList<>();
final ArrayDeque<Pair<JSNamedElement, Integer>> wrappers = new ArrayDeque<>(getWrappers(list));
while (!wrappers.isEmpty()) {
final Pair<JSNamedElement, Integer> pair = wrappers.removeFirst();
final JSNamedElement wrapper = pair.getFirst();
SearchScope scope = wrapper.getUseScope();
if (scope instanceof LocalSearchScope) {
scope = GlobalSearchScope.filesScope(project, Arrays.asList(((LocalSearchScope)scope).getVirtualFiles()));
}
final GlobalSearchScope scopeForSearch = JSUnusedGlobalSymbolsInspection.skipLibraryFiles(project, (GlobalSearchScope)scope);
final PsiSearchHelper.SearchCostResult cheapEnoughToSearch =
PsiSearchHelper.SERVICE.getInstance(project).isCheapEnoughToSearch(wrapper.getName(), scopeForSearch, null, null);
if (cheapEnoughToSearch == PsiSearchHelper.SearchCostResult.TOO_MANY_OCCURRENCES) {
continue;
}
final JSNamedElement namedElement = PsiTreeUtil.getParentOfType(wrapper.getNameIdentifier(), JSNamedElement.class);
final Collection<PsiReference> references = JSDefaultRenameProcessor.findReferencesForScope(namedElement, false, scopeForSearch);
for (PsiReference reference : references) {
if (!(reference instanceof PsiElement)) continue;
if (((PsiElement)reference).getParent() instanceof JSProperty &&
((JSProperty)((PsiElement)reference).getParent()).getValue() != null &&
((JSProperty)((PsiElement)reference).getParent()).getValue().equals(reference)) {
wrappers.add(Pair.create((JSProperty)((PsiElement)reference).getParent(), pair.getSecond()));
continue;
}
final JSCallExpression expression = PsiTreeUtil.getParentOfType((PsiElement)reference, JSCallExpression.class);
if (expression == null || !PsiTreeUtil.isAncestor(expression.getMethodExpression(), (PsiElement)reference, false)) continue;
final JSExpression[] arguments = expression.getArguments();
if (arguments.length > pair.getSecond() && arguments[pair.getSecond()] instanceof JSLiteralExpression) {
final JSLiteralExpression literal = (JSLiteralExpression)arguments[pair.getSecond()];
if (literal.isQuotedLiteral() && moduleName.equals(StringUtil.unquoteString(literal.getText()))) {
result.add(new JSResolveResult((PsiElement)reference));
}
}
}
}
return result.isEmpty() ? ResolveResult.EMPTY_ARRAY : result.toArray(ResolveResult.EMPTY_ARRAY);
}
private static List<Pair<JSNamedElement, Integer>> getWrappers(List<JSImplicitElement> list) {
final List<Pair<JSNamedElement, Integer>> wrappers = new ArrayList<>();
for (JSImplicitElement element : list) {
final Pair<JSNamedElement, Integer> wrapper = CachedValuesManager.getCachedValue(element, new MyCachedValueProvider(element));
if (wrapper != null) wrappers.add(wrapper);
}
return wrappers;
}
@NotNull
@Override
public Object[] getVariants() {
return ArrayUtil.EMPTY_OBJECT_ARRAY;
}
@Override
public boolean isSoft() {
return true;
}
private static class MyCachedValueProvider implements CachedValueProvider<Pair<JSNamedElement, Integer>> {
private final JSImplicitElement myElement;
public MyCachedValueProvider(JSImplicitElement element) {
myElement = element;
}
@Nullable
@Override
public Result<Pair<JSNamedElement, Integer>> compute() {
final JSCallExpression callExpression = PsiTreeUtil.getParentOfType(myElement, JSCallExpression.class);
if (callExpression == null) return null;
final JSExpression methodExpression = callExpression.getMethodExpression();
if (methodExpression instanceof JSReferenceExpression && JSSymbolUtil
.isAccurateReferenceExpressionName((JSReferenceExpression)methodExpression, ANGULAR, AngularJSIndexingHandler.MODULE)) {
if (callExpression.getArgumentList() == null || callExpression.getArgumentList().getArguments().length <= 1) {
return null;
}
final JSExpression firstArgument = callExpression.getArgumentList().getArguments()[0];
if (firstArgument instanceof JSReferenceExpression) {
final PsiElement resolve = ((JSReferenceExpression)firstArgument).resolve();
if (resolve != null && resolve.isValid() && resolve instanceof JSParameter) {
final JSFunction function = ((JSParameter)resolve).getDeclaringFunction();
if (function == null) return null;
final JSParameter[] variables = function.getParameterVariables();
int index = 0;
for (; index < variables.length; index++) {
JSParameter variable = variables[index];
if (variable == resolve) break;
}
if (index >= variables.length) return null;
if (function.getName() != null) {
return Result.create(Pair.create(function, index), myElement.getContainingFile());
}
}
}
}
return null;
}
}
}
}