package com.google.jstestdriver.idea.execution.generator;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import com.google.jstestdriver.idea.util.CastUtils;
import com.intellij.lang.javascript.index.JSIndexedRootProvider;
import com.intellij.lang.javascript.psi.JSElement;
import com.intellij.lang.javascript.psi.JSFile;
import com.intellij.lang.javascript.psi.JSLiteralExpression;
import com.intellij.openapi.editor.Document;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.Trinity;
import com.intellij.openapi.util.io.FileUtil;
import com.intellij.openapi.vfs.LocalFileSystem;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.psi.*;
import com.intellij.psi.search.FilenameIndex;
import com.intellij.psi.search.ProjectAndLibrariesScope;
import com.intellij.util.indexing.FileBasedIndex;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.io.File;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.*;
public class JstdConfigGenerator {
public static final JstdConfigGenerator INSTANCE = new JstdConfigGenerator();
@NotNull
public JstdGeneratedConfigStructure generateJstdConfigStructure(@NotNull JSFile jsFile) {
VirtualFile jsVirtualFile = jsFile.getVirtualFile();
if (jsVirtualFile == null) {
throw new RuntimeException("JavaScript file should have virtual file.");
}
JstdGeneratedConfigStructure configStructure = new JstdGeneratedConfigStructure();
DependencyContainer dependencyContainer = new DependencyContainer(jsFile.getProject());
fillDependencyMap(jsFile, dependencyContainer);
Set<VirtualFile> added = Sets.newHashSet();
addAllDependenciesInOrder(added, configStructure, jsVirtualFile, dependencyContainer);
return configStructure;
}
@NotNull
private JstdGeneratedConfigStructure generateJstdConfigStructure(@NotNull Project project, @NotNull File jsIoFile) {
VirtualFile jsVirtualFile = LocalFileSystem.getInstance().findFileByIoFile(jsIoFile);
if (jsVirtualFile == null) {
throw new RuntimeException("Could not find virtual file by io file " + jsIoFile.getAbsolutePath());
}
PsiFile psiFile = PsiManager.getInstance(project).findFile(jsVirtualFile);
JSFile jsPsiFile = CastUtils.tryCast(psiFile, JSFile.class);
if (jsPsiFile == null) {
throw new RuntimeException("Could not process " + jsVirtualFile.getPath() + " as JavaScript file.");
}
return generateJstdConfigStructure(jsPsiFile);
}
private void addAllDependenciesInOrder(Set<VirtualFile> added, JstdGeneratedConfigStructure configStructure, VirtualFile jsVirtualFile, DependencyContainer dependencyContainer) {
if (added.contains(jsVirtualFile)) {
return;
}
added.add(jsVirtualFile);
List<VirtualFile> dependentJsVirtualFiles = dependencyContainer.getDependentJsVirtualFiles(jsVirtualFile);
if (dependentJsVirtualFiles != null) {
for (VirtualFile dependentJsVirtualFile : dependentJsVirtualFiles) {
addAllDependenciesInOrder(added, configStructure, dependentJsVirtualFile, dependencyContainer);
}
}
configStructure.addLoadFile(new File(jsVirtualFile.getPath()));
}
private void fillDependencyMap(JSFile jsFile, DependencyContainer dependencyContainer) {
final VirtualFile jsVirtualFile = jsFile.getVirtualFile();
if (jsVirtualFile == null) {
return;
}
if (dependencyContainer.hasDependenciesFor(jsVirtualFile)) {
return;
}
if (dependencyContainer.handleSpecialCase(jsVirtualFile)) {
return;
}
if (dependencyContainer.getLibraryFileNames().contains(jsVirtualFile.getName())) {
return;
}
List<JSFile> dependentJsFiles = collectDependentJsFiles(jsFile, dependencyContainer);
List<VirtualFile> dependentVirtualFiles = Lists.newArrayList();
for (JSFile dependentJsFile : dependentJsFiles) {
dependentVirtualFiles.add(dependentJsFile.getVirtualFile());
}
dependencyContainer.registerDependency(jsVirtualFile, dependentVirtualFiles);
for (JSFile dependentJsFile : dependentJsFiles) {
fillDependencyMap(dependentJsFile, dependencyContainer);
}
}
@NotNull
private List<JSFile> collectDependentJsFiles(@NotNull JSFile jsFile, DependencyContainer dependencyContainer) {
Set<JSFile> dependentJsFiles = Sets.newLinkedHashSet();
collectDependentJsFilesByJsElement(jsFile, dependentJsFiles, dependencyContainer);
dependentJsFiles.remove(jsFile);
return Lists.newArrayList(dependentJsFiles);
}
private void collectDependentJsFilesByJsElement(PsiElement psiElement, Set<JSFile> dependentFiles, DependencyContainer dependencyContainer) {
if (psiElement instanceof JSElement) {
boolean resolve = !(psiElement instanceof JSLiteralExpression);
PsiReference[] psiReferences = resolve ? psiElement.getReferences() : PsiReference.EMPTY_ARRAY;
for (PsiReference psiReference : psiReferences) {
if (psiReference instanceof PsiPolyVariantReference) {
PsiPolyVariantReference psiPolyVariantReference = (PsiPolyVariantReference) psiReference;
ResolveResult[] resolveResults = psiPolyVariantReference.multiResolve(false);
for (ResolveResult resolveResult : resolveResults) {
PsiElement resolvedElement = resolveResult.getElement();
if (resolvedElement != null && resolveResult.isValidResult()) {
PsiFile resolvedPsiFile = resolvedElement.getContainingFile();
if (dependencyContainer.validatePsiFile(resolvedPsiFile)) {
dependentFiles.add((JSFile) resolvedPsiFile);
break;
}
}
}
}
}
}
for (PsiElement child : psiElement.getChildren()) {
collectDependentJsFilesByJsElement(child, dependentFiles, dependencyContainer);
}
}
private static Trinity<Integer, Integer, String> resolveInfo(PsiElement psiElement) {
Document document = PsiDocumentManager.getInstance(psiElement.getProject()).getDocument(psiElement.getContainingFile());
int textOffset = psiElement.getTextOffset();
if (textOffset < 0) {
System.err.println("text offset is negative: " + textOffset);
textOffset = 0;
}
assert document != null;
int lineNo = document.getLineNumber(textOffset);
int columnNo = psiElement.getTextOffset() - document.getLineStartOffset(lineNo);
String text = psiElement.getText();
return Trinity.create(lineNo, columnNo, text);
}
public File generateTempConfig(Project project, File jsIoFile) throws IOException {
JstdGeneratedConfigStructure configStructure = generateJstdConfigStructure(project, jsIoFile);
String lastName = jsIoFile.getName().replace('.', '-');
File tempConfigFile = FileUtil.createTempFile("generated-" + lastName, ".jstd");
PrintWriter writer = new PrintWriter(tempConfigFile);
try {
writer.print(configStructure.asFileContent());
} finally {
writer.close();
}
return tempConfigFile;
}
private static class DependencyContainer {
private Project myProject;
private Map<VirtualFile, List<VirtualFile>> myDependencyMap;
private Set<VirtualFile> myLibraryVirtualFiles;
private boolean myQUnitAdapterHandled = false;
private Set<String> myLibraryFileNames;
private DependencyContainer(Project project) {
myProject = project;
myDependencyMap = Maps.newHashMap();
myLibraryVirtualFiles = new JSIndexedRootProvider().getLibraryFiles(project);
myLibraryFileNames = Sets.newHashSet();
for (VirtualFile libraryVirtualFile : myLibraryVirtualFiles) {
myLibraryFileNames.add(libraryVirtualFile.getName());
}
}
public boolean hasDependenciesFor(VirtualFile jsVirtualFile) {
return myDependencyMap.containsKey(jsVirtualFile);
}
public boolean handleSpecialCase(VirtualFile jsVirtualFile) {
if ("QUnitAdapter.js".equals(jsVirtualFile.getName())) {
if (myQUnitAdapterHandled) {
return true;
}
myQUnitAdapterHandled = true;
final List<VirtualFile> virtualFiles = Lists.newArrayList();
FileBasedIndex.getInstance().processValues(FilenameIndex.NAME, "equiv.js", null, new FileBasedIndex.ValueProcessor<Void>() {
@Override
public boolean process(VirtualFile file, Void value) {
PsiFile psiFile = PsiManager.getInstance(myProject).findFile(file);
if (validatePsiFile(psiFile)) {
virtualFiles.add(file);
}
return true;
}
}, new ProjectAndLibrariesScope(myProject)
);
final String jsVirtualFileDirPath = jsVirtualFile.getParent().getPath();
Collections.sort(virtualFiles, new Comparator<VirtualFile>() {
@Override
public int compare(VirtualFile vf1, VirtualFile vf2) {
boolean sameDir1 = vf1.getParent().getPath().equals(jsVirtualFileDirPath);
boolean sameDir2 = vf2.getParent().getPath().equals(jsVirtualFileDirPath);
if (sameDir1) {
return -1;
}
if (sameDir2) {
return 1;
}
return 0;
}
});
if (!virtualFiles.isEmpty()) {
myDependencyMap.put(jsVirtualFile, Collections.singletonList(virtualFiles.get(0)));
}
return true;
}
return false;
}
public Set<String> getLibraryFileNames() {
return myLibraryFileNames;
}
public void registerDependency(VirtualFile jsVirtualFile, List<VirtualFile> dependentVirtualFiles) {
myDependencyMap.put(jsVirtualFile, dependentVirtualFiles);
}
public boolean validatePsiFile(@Nullable PsiFile psiFile) {
if (psiFile == null) {
return false;
}
VirtualFile vf = psiFile.getVirtualFile();
if (vf != null && !myLibraryVirtualFiles.contains(vf) && (psiFile instanceof JSFile)) {
JSFile jsFile = (JSFile) psiFile;
return !jsFile.isPredefined();
}
return false;
}
public List<VirtualFile> getDependentJsVirtualFiles(VirtualFile jsVirtualFile) {
List<VirtualFile> dependentVirtualFiles = myDependencyMap.get(jsVirtualFile);
return dependentVirtualFiles != null ? dependentVirtualFiles : Collections.<VirtualFile>emptyList();
}
}
}