package org.angularjs.codeInsight.router;
import com.intellij.lang.javascript.JavascriptLanguage;
import com.intellij.lang.javascript.modules.NodeModuleUtil;
import com.intellij.lang.javascript.psi.*;
import com.intellij.lang.javascript.psi.impl.JSOffsetBasedImplicitElement;
import com.intellij.lang.javascript.psi.stubs.JSImplicitElement;
import com.intellij.lang.javascript.psi.stubs.impl.JSImplicitElementImpl;
import com.intellij.openapi.fileTypes.LanguageFileType;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.Comparing;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.openapi.vfs.VfsUtilCore;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.psi.*;
import com.intellij.psi.impl.include.FileIncludeManager;
import com.intellij.psi.search.GlobalSearchScope;
import com.intellij.psi.util.PsiTreeUtil;
import com.intellij.util.CommonProcessors;
import com.intellij.util.ObjectUtils;
import com.intellij.util.containers.ContainerUtil;
import com.intellij.util.indexing.FileBasedIndex;
import org.angularjs.index.*;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.*;
/**
* @author Irina.Chernushina on 3/8/2016.
*/
public class AngularUiRouterDiagramBuilder {
private final List<UiRouterState> myStates;
private final Map<VirtualFile, Template> myTemplatesMap;
private final Map<VirtualFile, RootTemplate> myRootTemplates;
@NotNull private final Project myProject;
private SmartPointerManager mySmartPointerManager;
private final Map<PsiFile, Set<VirtualFile>> myModuleRecursiveDependencies;
private Map<VirtualFile, Map<String, UiRouterState>> myRootTemplates2States;
private Map<VirtualFile, Map<String, UiRouterState>> myDefiningFiles2States;
// todo different scope
public AngularUiRouterDiagramBuilder(@NotNull final Project project) {
myProject = project;
myStates = new ArrayList<>();
myTemplatesMap = new HashMap<>();
myRootTemplates = new HashMap<>();
mySmartPointerManager = SmartPointerManager.getInstance(myProject);
myModuleRecursiveDependencies = new HashMap<>();
}
public void build() {
addStatesFromIndex();
addGenericStates();
getRootPages();
groupStates();
}
private void addStatesFromIndex() {
final Collection<String> stateIds = AngularIndexUtil.getAllKeys(AngularUiRouterStatesIndex.KEY, myProject);
for (String id : stateIds) {
if (id.startsWith(".")) continue;
final CommonProcessors.CollectProcessor<JSImplicitElement> processor = new CommonProcessors.CollectProcessor<>();
AngularIndexUtil.multiResolve(myProject, AngularUiRouterStatesIndex.KEY, id, processor);
for (JSImplicitElement element : processor.getResults()) {
final UiRouterState state = new UiRouterState(id, element.getContainingFile().getVirtualFile());
if (!element.getContainingFile().getLanguage().isKindOf(JavascriptLanguage.INSTANCE)
&& PsiTreeUtil.getParentOfType(element, JSEmbeddedContent.class) != null) {
createRootTemplatesForEmbedded(element.getContainingFile());
}
final JSCallExpression call = findWrappingCallExpression(element);
if (call != null) {
final JSReferenceExpression methodExpression = ObjectUtils.tryCast(call.getMethodExpression(), JSReferenceExpression.class);
if (methodExpression != null &&
methodExpression.getQualifier() != null &&
"state".equals(methodExpression.getReferenceName())) {
final JSExpression[] arguments = call.getArguments();
if (arguments.length > 0 && PsiTreeUtil.isAncestor(arguments[0], element.getNavigationElement(), false)) {
state.setPointer(mySmartPointerManager.createSmartPsiElementPointer(arguments[0]));
if (arguments.length > 1 && arguments[1] instanceof JSObjectLiteralExpression) {
final JSObjectLiteralExpression object = (JSObjectLiteralExpression)arguments[1];
fillStateParameters(state, object);
} else if (arguments[0] instanceof JSObjectLiteralExpression) {
final JSObjectLiteralExpression object = (JSObjectLiteralExpression)arguments[0];
final JSProperty name = object.findProperty("name");
if (name != null && PsiTreeUtil.isAncestor(name, element.getNavigationElement(), false)) {
fillStateParameters(state, object);
}
}
}
}
}
myStates.add(state);
}
}
}
private void addGenericStates() {
final List<JSObjectLiteralExpression> freeStates = new AngularRouterStateLoader(myProject).loadFreelyDefinedStates();
for (JSObjectLiteralExpression state : freeStates) {
final JSProperty name = state.findProperty("name");
if (name != null && name.getValue() instanceof JSLiteralExpression && ((JSLiteralExpression)name.getValue()).isQuotedLiteral()) {
final UiRouterState uiState = new UiRouterState(StringUtil.unquoteString(name.getValue().getText()),
name.getContainingFile().getVirtualFile());
uiState.setGeneric(true);
uiState.setPointer(mySmartPointerManager.createSmartPsiElementPointer(name));
fillStateParameters(uiState, state);
if (!myStates.contains(uiState)) myStates.add(uiState);
}
}
}
@Nullable
public static JSCallExpression findWrappingCallExpression(JSImplicitElement element) {
if (element.getNavigationElement() instanceof JSCallExpression) return (JSCallExpression)element.getNavigationElement();
JSCallExpression call = PsiTreeUtil.getParentOfType(element.getNavigationElement(), JSCallExpression.class);
if (call == null) {
final PsiElement elementAt =
element.getContainingFile().findElementAt(element.getNavigationElement().getTextRange().getEndOffset() - 1);
if (elementAt != null) {
call = PsiTreeUtil.getParentOfType(elementAt, JSCallExpression.class);
}
}
return call;
}
private void groupStates() {
// root template file vs. state
// but the same state can be used for several root templates
myRootTemplates2States = new HashMap<>();
final Set<UiRouterState> statesUsedInRoots = new HashSet<>();
for (Map.Entry<VirtualFile, RootTemplate> entry : myRootTemplates.entrySet()) {
final Set<VirtualFile> modulesFiles = entry.getValue().getModulesFiles();
for (UiRouterState state : myStates) {
final PsiElement element = entry.getValue().getPointer().getElement();
if (modulesFiles.contains(state.getFile()) || element != null && element.getContainingFile().getVirtualFile().equals(state.getFile())) {
putState2map(entry.getKey(), state, myRootTemplates2States);
statesUsedInRoots.add(state);
}
}
}
myDefiningFiles2States = new HashMap<>();
for (UiRouterState state : myStates) {
if (statesUsedInRoots.contains(state)) continue;
if (state.isGeneric()) {
putState2map(myRootTemplates.keySet().iterator().next(), state, myRootTemplates2States);
} else putState2map(state.getFile(), state, myDefiningFiles2States);
}
}
private static void putState2map(@NotNull final VirtualFile rootFile, @NotNull final UiRouterState state,
@NotNull final Map<VirtualFile, Map<String, UiRouterState>> rootMap) {
Map<String, UiRouterState> map = rootMap.get(rootFile);
if (map == null) rootMap.put(rootFile, (map = new HashMap<>()));
if (map.containsKey(state.getName())) {
final UiRouterState existing = map.get(state.getName());
if (!Comparing.equal(existing.getPointer(), state.getPointer()) && state.getPointer() != null) {
existing.addDuplicateDefinition(state);
}
} else {
map.put(state.getName(), state);
}
}
private void getRootPages() {
final List<VirtualFile> roots = new ArrayList<>();
Collections.sort(roots, (o1, o2) -> Integer.compare(o2.getUrl().length(), o1.getUrl().length()));
final Map<PsiFile, AngularNamedItemDefinition> files = new HashMap<>();
final FileBasedIndex instance = FileBasedIndex.getInstance();
final Collection<String> keys = instance.getAllKeys(AngularAppIndex.ANGULAR_APP_INDEX, myProject);
if (keys.isEmpty()) return;
final PsiManager psiManager = PsiManager.getInstance(myProject);
final GlobalSearchScope projectScope = GlobalSearchScope.projectScope(myProject);
for (String key : keys) {
instance.processValues(AngularAppIndex.ANGULAR_APP_INDEX, key, null, (file, value) -> {
final PsiFile psiFile = psiManager.findFile(file);
if (psiFile != null) {
files.put(psiFile, value);
}
return true;
}, projectScope);
}
for (Map.Entry<PsiFile, AngularNamedItemDefinition> entry : files.entrySet()) {
final PsiFile file = entry.getKey();
final String relativeUrl = findPossibleRelativeUrl(roots, file.getVirtualFile());
// not clear how then it can be part of application
if (relativeUrl == null) continue;
final Template template = readTemplateFromFile(myProject, relativeUrl, file);
final String mainModule = entry.getValue().getName();
final Set<VirtualFile> moduleFiles = getModuleFiles(file, mainModule);
final RootTemplate rootTemplate = new RootTemplate(mySmartPointerManager.createSmartPsiElementPointer(file),
relativeUrl, template, moduleFiles);
myRootTemplates.put(file.getVirtualFile(), rootTemplate);
}
}
private void createRootTemplatesForEmbedded(@NotNull PsiFile containingFile) {
final Template template = readTemplateFromFile(myProject, "/", containingFile);
final RootTemplate rootTemplate = new RootTemplate(mySmartPointerManager.createSmartPsiElementPointer(containingFile),
"/", template, Collections.singleton(containingFile.getVirtualFile()));
myRootTemplates.put(containingFile.getVirtualFile(), rootTemplate);
}
private static class NonCyclicQueue<T> {
private final Set<T> processed = new HashSet<>();
private final ArrayDeque<T> toProcess = new ArrayDeque<>();
public void add(@NotNull T t) {
if (processed.contains(t) || !check(t)) return;
processed.add(t);
toProcess.add(t);
}
protected boolean check(T t) {
return true;
}
public void addAll(final Collection<T> collection) {
for (T t : collection) {
add(t);
}
}
public boolean isEmpty() {
return toProcess.isEmpty();
}
@Nullable
public T removeNext() {
return toProcess.isEmpty() ? null : toProcess.remove();
}
public Set<T> getProcessed() {
return processed;
}
}
@NotNull
private Set<VirtualFile> getModuleFiles(PsiFile file, String mainModule) {
Set<VirtualFile> moduleFiles = myModuleRecursiveDependencies.get(file);
if (moduleFiles != null) return moduleFiles;
final NonCyclicQueue<String> modulesQueue = new NonCyclicQueue<>();
final NonCyclicQueue<VirtualFile> filesQueue = new NonCyclicQueue<VirtualFile>() {
@Override
protected boolean check(VirtualFile file) {
// do not add lib (especially angular) files
return !NodeModuleUtil.isFromNodeModules(myProject, file);
}
};
if (!StringUtil.isEmptyOrSpaces(mainModule)) {
modulesQueue.add(mainModule);
}
filesQueue.add(file.getVirtualFile());
while (!modulesQueue.isEmpty()) {
final String moduleName = modulesQueue.removeNext();
moduleDependenciesStep(moduleName, filesQueue, modulesQueue);
}
while (!filesQueue.isEmpty()) {
final VirtualFile moduleFile = filesQueue.removeNext();
filesDependenciesStep(moduleFile, filesQueue);
}
Set<VirtualFile> processed = filesQueue.getProcessed();
final GlobalSearchScope projectScope = GlobalSearchScope.projectScope(myProject);
processed =
new HashSet<>(ContainerUtil.filter(processed, file1 -> file1.getFileType() instanceof LanguageFileType && ((LanguageFileType)file1
.getFileType()).getLanguage().isKindOf(
JavascriptLanguage.INSTANCE) && projectScope.contains(file1)));
myModuleRecursiveDependencies.put(file, processed);
return processed;
}
private void moduleDependenciesStep(String mainModule, NonCyclicQueue<VirtualFile> filesQueue, NonCyclicQueue<String> modulesQueue) {
addContainingFile(filesQueue, mainModule);
if (!StringUtil.isEmptyOrSpaces(mainModule)) {
final JSImplicitElement element = AngularIndexUtil.resolve(myProject, AngularModuleIndex.KEY, mainModule);
if (element != null) {
final JSCallExpression callExpression = PsiTreeUtil.getParentOfType(element, JSCallExpression.class);
if (callExpression == null) return;
final List<String> dependenciesInModuleDeclaration = AngularModuleIndex.findDependenciesInModuleDeclaration(callExpression);
if (dependenciesInModuleDeclaration != null) {
for (String module : dependenciesInModuleDeclaration) {
modulesQueue.add(module);
addContainingFile(filesQueue, module);
}
}
}
}
}
private void addContainingFile(@NotNull final NonCyclicQueue<VirtualFile> filesQueue, @NotNull final String module) {
final CommonProcessors.CollectProcessor<JSImplicitElement> collectProcessor = new CommonProcessors.CollectProcessor<>();
AngularIndexUtil.multiResolve(myProject, AngularModuleIndex.KEY, module, collectProcessor);
if (collectProcessor.getResults().size() != 1) return;
for (JSImplicitElement element : collectProcessor.getResults()) {
if (element != null && element.getNavigationElement() != null && element.getNavigationElement().getContainingFile() != null) {
final VirtualFile file = element.getNavigationElement().getContainingFile().getVirtualFile();
// prefer library resolves
if (NodeModuleUtil.isFromNodeModules(myProject, file)) return;
}
}
final JSImplicitElement element = collectProcessor.getResults().iterator().next();
if (element != null && element.getNavigationElement() != null && element.getNavigationElement().getContainingFile() != null) {
filesQueue.add(element.getNavigationElement().getContainingFile().getVirtualFile());
}
}
private void filesDependenciesStep(VirtualFile file, NonCyclicQueue<VirtualFile> filesQueue) {
final VirtualFile[] includedFiles = FileIncludeManager.getManager(myProject).getIncludedFiles(file, true, true);
//take all included, since there can be also html includes (??? exclude css & like)
filesQueue.addAll(Arrays.asList(includedFiles));
}
private String findPossibleRelativeUrl(@NotNull final List<VirtualFile> roots, @NotNull final VirtualFile file) {
VirtualFile contentRoot = null;
for (VirtualFile root : roots) {
if (root.equals(VfsUtilCore.getCommonAncestor(root, file))) {
contentRoot = root;
break;
}
}
final VirtualFile ancestor = contentRoot == null ? myProject.getBaseDir() : contentRoot;
if (ancestor == null) return null;
final String relativePath = VfsUtilCore.getRelativePath(file, ancestor);
return relativePath == null ? null : AngularUiRouterGraphBuilder.normalizeTemplateUrl(relativePath);
}
private void fillStateParameters(UiRouterState state, JSObjectLiteralExpression object) {
final String url = getPropertyValueIfExists(object, "url");
if (url != null) {
state.setUrl(StringUtil.unquoteString(url));
}
final String parentKey = getPropertyValueIfExists(object, "parent");
if (parentKey != null) {
state.setParentName(parentKey);
}
final String templateUrl = getPropertyValueIfExists(object, "templateUrl");
VirtualFile templateFile = null;
if (templateUrl != null) {
state.setTemplateUrl(templateUrl);
final JSProperty urlProperty = object.findProperty("templateUrl");
state.setTemplateFile(parseTemplate(templateUrl, urlProperty));
}
final JSProperty template = object.findProperty("template");
if (templateUrl == null && object.findProperty("templateUrl") != null ||
template != null || object.findProperty("templateProvider") != null) {
state.setHasTemplateDefined(true);
}
if (template != null) {
final PsiElement templateDefinition = findTemplateDefinitionObject(template);
if (templateDefinition != null) state.setTemplatePointer(mySmartPointerManager.createSmartPsiElementPointer(templateDefinition));
}
final JSProperty views = object.findProperty("views");
if (views != null) {
final JSExpression value = views.getValue();
if (value != null && value instanceof JSObjectLiteralExpression) {
final JSProperty[] viewsProperties = ((JSObjectLiteralExpression)value).getProperties();
if (viewsProperties != null && viewsProperties.length > 0) {
final List<UiView> viewsList = new ArrayList<>();
for (JSProperty property : viewsProperties) {
if (property.getName() != null && property.getValue() != null) {
viewsList.add(processView(property));
}
}
state.setViews(viewsList);
}
}
}
final JSProperty abstractProperty = object.findProperty("abstract");
if (abstractProperty != null && abstractProperty.getValue() instanceof JSLiteralExpression &&
((JSLiteralExpression)abstractProperty.getValue()).isBooleanLiteral() &&
Boolean.TRUE.equals(((JSLiteralExpression)abstractProperty.getValue()).getValue())) {
state.setAbstract(true);
}
}
@Nullable
private static PsiElement findTemplateDefinitionObject(@NotNull final JSProperty template) {
final JSExpression value = template.getValue();
if (value instanceof JSLiteralExpression) return value;
if (value instanceof JSReferenceExpression) {
final PsiElement resolve = ((JSReferenceExpression)value).resolve();
if (resolve != null && resolve.isValid() && resolve instanceof JSVariable) {
if (((JSVariable)resolve).getInitializer() instanceof JSLiteralExpression) return ((JSVariable)resolve).getInitializer();
}
}
return null;
}
@Nullable
private VirtualFile parseTemplate(@NotNull final String url, @Nullable JSProperty urlProperty) {
PsiFile templateFile = null;
Template template = null;
if (urlProperty != null && urlProperty.getValue() != null) {
int offset = urlProperty.getValue().getTextRange().getEndOffset() - 1;
final PsiReference reference = urlProperty.getContainingFile().findReferenceAt(offset);
if (reference != null) {
final PsiElement templateFileElement = reference.resolve();
if (templateFileElement != null && templateFileElement.isValid()) {
templateFile = templateFileElement.getContainingFile();
if (myTemplatesMap.containsKey(templateFile.getVirtualFile())) return templateFile.getVirtualFile();
template = readTemplateFromFile(urlProperty.getProject(), url, templateFile);
myTemplatesMap.put(templateFile.getVirtualFile(), template);
return templateFile.getVirtualFile();
}
}
}
return null;
}
@NotNull
static Template readTemplateFromFile(@NotNull Project project, @NotNull String url, PsiElement templateElement) {
final PsiFile templateFile = templateElement.getContainingFile();
final Map<String, SmartPsiElementPointer<PsiElement>> placeholders = new HashMap<>();
final Set<String> placeholdersSet = new HashSet<>();
final FileBasedIndex instance = FileBasedIndex.getInstance();
final GlobalSearchScope scope = GlobalSearchScope.fileScope(project, templateFile.getVirtualFile());
instance.processAllKeys(AngularUiRouterViewsIndex.UI_ROUTER_VIEWS_CACHE_INDEX, view -> {
placeholdersSet.add(view);
return true;
}, scope, null);
final SmartPointerManager smartPointerManager = SmartPointerManager.getInstance(project);
for (String key : placeholdersSet) {
instance.processValues(AngularUiRouterViewsIndex.UI_ROUTER_VIEWS_CACHE_INDEX, key, null,
(file, value) -> {
final JSImplicitElementImpl.Builder builder = new JSImplicitElementImpl.Builder(
JSQualifiedNameImpl.fromQualifiedName(key), null);
final JSOffsetBasedImplicitElement implicitElement =
new JSOffsetBasedImplicitElement(builder, (int)value.getStartOffset(), templateFile);
if (templateElement instanceof PsiFile ||
PsiTreeUtil.isAncestor(templateElement, implicitElement, false)) {
placeholders.put(key, smartPointerManager.createSmartPsiElementPointer(implicitElement));
}
return true;
}, scope);
}
final Template template = new Template(url, smartPointerManager.createSmartPsiElementPointer(templateElement));
template.setViewPlaceholders(placeholders);
return template;
}
private UiView processView(@NotNull final JSProperty property) {
final String name = property.getName();
final JSExpression value = property.getValue();
final JSObjectLiteralExpression expression = ObjectUtils.tryCast(value, JSObjectLiteralExpression.class);
String templateUrl = null;
VirtualFile templateFile = null;
if (expression != null) {
templateUrl = getPropertyValueIfExists(expression, "templateUrl");
if (templateUrl != null) {
final JSProperty urlProperty = expression.findProperty("templateUrl");
templateFile = parseTemplate(templateUrl, urlProperty);
}
}
final UiView view = new UiView(name, templateUrl, templateFile,
property.getNameIdentifier() == null
? null
: mySmartPointerManager.createSmartPsiElementPointer(property.getNameIdentifier()));
if (expression != null) {
final JSProperty template = expression.findProperty("template");
if (template != null) {
final PsiElement templateDefinition = findTemplateDefinitionObject(template);
if (templateDefinition != null) view.setTemplatePointer(mySmartPointerManager.createSmartPsiElementPointer(templateDefinition));
}
}
return view;
}
@Nullable
private static String getPropertyValueIfExists(@NotNull final JSObjectLiteralExpression object, @NotNull final String name) {
final JSProperty urlProperty = object.findProperty(name);
if (urlProperty != null && urlProperty.getValue() instanceof JSLiteralExpression &&
((JSLiteralExpression)urlProperty.getValue()).isQuotedLiteral()) {
return StringUtil.unquoteString(urlProperty.getValue().getText());
}
return null;
}
public Map<VirtualFile, Template> getTemplatesMap() {
return myTemplatesMap;
}
public Map<VirtualFile, RootTemplate> getRootTemplates() {
return myRootTemplates;
}
public Map<VirtualFile, Map<String, UiRouterState>> getRootTemplates2States() {
return myRootTemplates2States;
}
public Map<VirtualFile, Map<String, UiRouterState>> getDefiningFiles2States() {
return myDefiningFiles2States;
}
}