/*
* Copyright 2012-2014 Sergey Ignatov
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.intellij.erlang.compilation;
import com.intellij.compiler.server.BuildManager;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.compiler.CompileContext;
import com.intellij.openapi.compiler.CompileTask;
import com.intellij.openapi.compiler.CompilerMessageCategory;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.module.Module;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.Computable;
import com.intellij.openapi.util.Couple;
import com.intellij.openapi.util.JDOMUtil;
import com.intellij.openapi.vfs.VfsUtilCore;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.psi.PsiFile;
import com.intellij.psi.PsiManager;
import com.intellij.psi.search.GlobalSearchScope;
import com.intellij.util.ObjectUtils;
import com.intellij.util.SystemProperties;
import com.intellij.util.containers.ContainerUtil;
import com.intellij.util.graph.DFSTBuilder;
import com.intellij.util.graph.GraphGenerator;
import com.intellij.util.xmlb.SkipDefaultValuesSerializationFilters;
import com.intellij.util.xmlb.XmlSerializationException;
import com.intellij.util.xmlb.XmlSerializer;
import org.intellij.erlang.configuration.ErlangCompilerSettings;
import org.intellij.erlang.facet.ErlangFacet;
import org.intellij.erlang.index.ErlangModuleIndex;
import org.intellij.erlang.jps.builder.ErlangBuilderUtil;
import org.intellij.erlang.jps.builder.ErlangFileDescriptor;
import org.intellij.erlang.jps.builder.ErlangProjectBuildOrder;
import org.intellij.erlang.psi.ErlangFile;
import org.intellij.erlang.psi.impl.ErlangPsiImplUtil;
import org.jdom.Document;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.annotations.TestOnly;
import java.io.File;
import java.io.IOException;
import java.util.*;
import static org.intellij.erlang.utils.ErlangModulesUtil.getErlangHeaderFiles;
import static org.intellij.erlang.utils.ErlangModulesUtil.getErlangModuleFiles;
public class ErlangPrepareDependenciesCompileTask implements CompileTask {
private static final Logger LOG = Logger.getInstance(ErlangPrepareDependenciesCompileTask.class);
@Override
public boolean execute(final CompileContext context) {
Project project = context.getProject();
if (ErlangCompilerSettings.getInstance(project).isUseRebarCompilerEnabled()) {
// delegate dependencies resolution to rebar
return true;
}
LOG.info("Prepare build order for project " + project.getName());
File projectSystemDirectory = BuildManager.getInstance().getProjectSystemDirectory(project);
if (projectSystemDirectory == null) {
addPrepareDependenciesFailedMessage(context);
return true;
}
ErlangProjectBuildOrder projectBuildOrder = ApplicationManager.getApplication().runReadAction((Computable<ErlangProjectBuildOrder>) () -> getProjectBuildOrder(context));
if (projectBuildOrder == null) {
return false; // errors are reported to context.
}
writeBuildOrder(context, projectSystemDirectory, projectBuildOrder);
return true;
}
private static void writeBuildOrder(@NotNull CompileContext context,
@NotNull File projectSystemDirectory,
@NotNull ErlangProjectBuildOrder projectBuildOrder) {
try {
LOG.debug("Serialize build order");
Document serializedDocument = new Document(XmlSerializer.serialize(projectBuildOrder, new SkipDefaultValuesSerializationFilters()));
File parentDir = new File(projectSystemDirectory, ErlangBuilderUtil.BUILDER_DIRECTORY);
//noinspection ResultOfMethodCallIgnored
parentDir.mkdirs();
File file = new File(parentDir, ErlangBuilderUtil.BUILD_ORDER_FILE_NAME);
LOG.debug("Write build order to " + file.getAbsolutePath());
JDOMUtil.writeDocument(serializedDocument, file, SystemProperties.getLineSeparator());
}
catch (XmlSerializationException e) {
LOG.error("Can't serialize build order object.", e);
addPrepareDependenciesFailedMessage(context);
}
catch (IOException e) {
LOG.warn("Some I/O errors occurred while writing build orders to file", e);
addPrepareDependenciesFailedMessage(context);
}
}
private static void addPrepareDependenciesFailedMessage(@NotNull CompileContext context) {
context.addMessage(CompilerMessageCategory.WARNING, "Failed to submit dependencies info to compiler.", null, -1, -1);
}
@TestOnly
@NotNull
static List<ErlangFileDescriptor> getBuildOrder(@NotNull Module module) throws CyclicDependencyFoundException {
return getTopologicallySortedFileDescriptors(module);
}
@Nullable
private static ErlangProjectBuildOrder getProjectBuildOrder(@NotNull CompileContext context) {
try {
Module[] modulesToCompile = context.getCompileScope().getAffectedModules();
return new ErlangProjectBuildOrder(getTopologicallySortedFileDescriptors(modulesToCompile));
}
catch (CyclicDependencyFoundException e) {
String message = "Cyclic erlang module dependency detected. Check files " +
e.getFirstFileInCycle() + " and " + e.getLastFileInCycle() +
"or their dependencies(parse_transform, behaviour, include)";
LOG.debug(message, e);
context.addMessage(CompilerMessageCategory.ERROR, message, null, -1, -1);
return null;
}
}
@NotNull
private static List<String> getGlobalParseTransforms(@NotNull Module module) {
ErlangFacet erlangFacet = ErlangFacet.getFacet(module);
return erlangFacet != null ? erlangFacet.getConfiguration().getParseTransforms() : ContainerUtil.<String>emptyList();
}
@NotNull
private static List<ErlangFileDescriptor> getTopologicallySortedFileDescriptors(@NotNull Module... modulesToCompile) throws CyclicDependencyFoundException {
final ErlangFilesDependencyGraph semiGraph = ErlangFilesDependencyGraph.createSemiGraph(modulesToCompile);
DFSTBuilder<String> builder = new DFSTBuilder<>(GraphGenerator.create(semiGraph));
if (!builder.isAcyclic()) {
throw new CyclicDependencyFoundException(builder.getCircularDependency());
}
return ContainerUtil.map(builder.getSortedNodes(), filePath -> new ErlangFileDescriptor(filePath, semiGraph.getDependencies(filePath)));
}
@NotNull
private static String getPath(@NotNull VirtualFile file) {
File ioFile = VfsUtilCore.virtualToIoFile(file);
return ErlangBuilderUtil.getPath(ioFile);
}
private static class ErlangFilesDependencyGraph implements GraphGenerator.SemiGraph<String> {
private final Project myProject;
private final PsiManager myPsiManager;
private final Set<String> myHeaders;
private final Map<String, List<String>> myPathsToDependenciesMap = ContainerUtil.newHashMap();
private ErlangFilesDependencyGraph(@NotNull Module[] modulesToCompile) {
assert modulesToCompile.length > 0;
myProject = modulesToCompile[0].getProject();
myPsiManager = PsiManager.getInstance(myProject);
myHeaders = collectHeaderPaths(modulesToCompile);
for (Module module : modulesToCompile) {
buildDependenciesMap(module);
}
}
@NotNull
public static ErlangFilesDependencyGraph createSemiGraph(@NotNull Module[] modulesToCompile) {
return new ErlangFilesDependencyGraph(modulesToCompile);
}
@NotNull
private static Set<String> collectHeaderPaths(@NotNull Module[] modulesToCompile) {
Set<String> erlangHeaders = ContainerUtil.newHashSet();
for (Module module : modulesToCompile) {
erlangHeaders.addAll(getHeaders(module, false));
erlangHeaders.addAll(getHeaders(module, true));
}
return erlangHeaders;
}
@NotNull
private static List<String> getHeaders(Module module, boolean onlyTestModules) {
return ContainerUtil.map(getErlangHeaderFiles(module, onlyTestModules), ErlangPrepareDependenciesCompileTask::getPath);
}
@Override
public Collection<String> getNodes() {
return myPathsToDependenciesMap.keySet();
}
@Override
public Iterator<String> getIn(@NotNull String filePath) {
return myPathsToDependenciesMap.get(filePath).iterator();
}
@NotNull
public List<String> getDependencies(@NotNull String filePath) {
return ObjectUtils.assertNotNull(myPathsToDependenciesMap.get(filePath));
}
private void buildDependenciesMap(@NotNull Module module) {
List<String> globalParseTransform = resolvePathsFromNames(getGlobalParseTransforms(module), module);
buildDependenciesMap(module, getErlangHeaderFiles(module, false), ContainerUtil.<String>emptyList());
buildDependenciesMap(module, getErlangHeaderFiles(module, true), ContainerUtil.<String>emptyList());
buildDependenciesMap(module, getErlangModuleFiles(module, false), globalParseTransform);
buildDependenciesMap(module, getErlangModuleFiles(module, true), globalParseTransform);
}
private void buildDependenciesMap(@NotNull Module module,
@NotNull Collection<VirtualFile> erlangFiles,
@NotNull List<String> globalParseTransforms) {
for (VirtualFile file : erlangFiles) {
Set<String> dependencies = ContainerUtil.newHashSet();
ErlangFile psi = getErlangFile(file);
addDeclaredDependencies(module, psi, dependencies);
dependencies.addAll(globalParseTransforms);
myPathsToDependenciesMap.put(getPath(file), ContainerUtil.newArrayList(dependencies));
}
}
@NotNull
private ErlangFile getErlangFile(@NotNull VirtualFile virtualFile) {
PsiFile psiFile = myPsiManager.findFile(virtualFile);
return ObjectUtils.assertNotNull(ObjectUtils.tryCast(psiFile, ErlangFile.class));
}
private void addDeclaredDependencies(@NotNull Module module,
@NotNull ErlangFile erlangModule,
@NotNull Set<String> dependencies) {
dependencies.addAll(getDeclaredParseTransformPaths(module, erlangModule));
dependencies.addAll(getDeclaredBehaviourPaths(module, erlangModule));
dependencies.addAll(getDeclaredIncludePaths(erlangModule));
}
@NotNull
private List<String> getDeclaredParseTransformPaths(@NotNull Module module, @NotNull ErlangFile erlangModule) {
Set<String> pt = ContainerUtil.newHashSet();
erlangModule.addDeclaredParseTransforms(pt);
return resolvePathsFromNames(pt, module);
}
@NotNull
private List<String> getDeclaredBehaviourPaths(@NotNull Module module, @NotNull ErlangFile erlangModule) {
Set<String> behaviours = ContainerUtil.newHashSet();
ErlangPsiImplUtil.addDeclaredBehaviourModuleNames(erlangModule, behaviours);
return resolvePathsFromNames(behaviours, module);
}
@NotNull
private List<String> resolvePathsFromNames(@NotNull Collection<String> erlangModuleNames, @NotNull Module module) {
List<String> paths = ContainerUtil.newArrayList();
for (String erlangModuleName : erlangModuleNames) {
paths.addAll(getPathsFromModuleName(erlangModuleName, module));
}
return paths;
}
@NotNull
private List<String> getDeclaredIncludePaths(@NotNull ErlangFile file) {
return ContainerUtil.mapNotNull(ErlangPsiImplUtil.getDirectlyIncludedFiles(file), erlangFile -> {
VirtualFile file1 = erlangFile.getVirtualFile();
String path = file1 != null ? getPath(file1) : null;
return path != null && myHeaders.contains(path) ? path : null;
});
}
@NotNull
private List<String> getPathsFromModuleName(@NotNull String erlangModuleName, @NotNull Module module) {
List<ErlangFile> filesByName = ErlangModuleIndex.getFilesByName(myProject,
erlangModuleName,
GlobalSearchScope.moduleWithDependenciesScope(module));
return ContainerUtil.mapNotNull(filesByName, erlangFile -> {
VirtualFile virtualFile = erlangFile.getVirtualFile();
return virtualFile != null ? getPath(virtualFile) : null;
});
}
}
static class CyclicDependencyFoundException extends Exception {
private final Couple<String> myCyclicDependencies;
CyclicDependencyFoundException(@NotNull Couple<String> cyclicDependencies) {
this.myCyclicDependencies = cyclicDependencies;
}
@NotNull
public String getFirstFileInCycle() {
return myCyclicDependencies.getFirst();
}
@NotNull
public String getLastFileInCycle() {
return myCyclicDependencies.getSecond();
}
}
}