/*
* Copyright 2015-present Facebook, Inc.
*
* 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 com.facebook.buck.ide.intellij;
import com.facebook.buck.ide.intellij.aggregation.AggregationModule;
import com.facebook.buck.ide.intellij.aggregation.AggregationModuleFactory;
import com.facebook.buck.ide.intellij.aggregation.AggregationTree;
import com.facebook.buck.ide.intellij.model.DependencyType;
import com.facebook.buck.ide.intellij.model.IjLibrary;
import com.facebook.buck.ide.intellij.model.IjLibraryFactory;
import com.facebook.buck.ide.intellij.model.IjModule;
import com.facebook.buck.ide.intellij.model.IjModuleFactory;
import com.facebook.buck.ide.intellij.model.IjProjectConfig;
import com.facebook.buck.ide.intellij.model.IjProjectElement;
import com.facebook.buck.io.ProjectFilesystem;
import com.facebook.buck.model.BuildTarget;
import com.facebook.buck.rules.CommonDescriptionArg;
import com.facebook.buck.rules.TargetGraph;
import com.facebook.buck.rules.TargetNode;
import com.facebook.buck.util.MoreCollectors;
import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableListMultimap;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Stream;
public final class IjModuleGraphFactory {
/**
* Create all the modules we are capable of representing in IntelliJ from the supplied graph.
*
* @param targetGraph graph whose nodes will be converted to {@link IjModule}s.
* @return map which for every BuildTarget points to the corresponding IjModule. Multiple
* BuildTarget can point to one IjModule (many:one mapping), the BuildTargets which can't be
* prepresented in IntelliJ are missing from this mapping.
*/
private static ImmutableMap<BuildTarget, IjModule> createModules(
ProjectFilesystem projectFilesystem,
TargetGraph targetGraph,
IjModuleFactory moduleFactory,
AggregationModuleFactory aggregationModuleFactory,
final int minimumPathDepth,
ImmutableSet<String> ignoredTargetLabels) {
ImmutableListMultimap<Path, TargetNode<?, ?>> baseTargetPathMultimap =
targetGraph
.getNodes()
.stream()
.filter(
input ->
SupportedTargetTypeRegistry.isTargetTypeSupported(
input.getDescription().getClass()))
.filter(
targetNode -> {
CommonDescriptionArg arg = (CommonDescriptionArg) targetNode.getConstructorArg();
return !arg.labelsContainsAnyOf(ignoredTargetLabels);
})
// IntelliJ doesn't support referring to source files which aren't below the root of the
// project. Filter out those cases proactively, so that we don't try to resolve files
// relative to the wrong ProjectFilesystem.
// Maybe one day someone will fix this.
.filter(targetNode -> isInRootCell(projectFilesystem, targetNode))
.collect(
MoreCollectors.toImmutableListMultimap(
targetNode -> targetNode.getBuildTarget().getBasePath(),
targetNode -> targetNode));
AggregationTree aggregationTree =
createAggregationTree(aggregationModuleFactory, baseTargetPathMultimap);
aggregationTree.aggregateModules(minimumPathDepth);
ImmutableMap.Builder<BuildTarget, IjModule> moduleByBuildTarget = new ImmutableMap.Builder<>();
aggregationTree
.getModules()
.stream()
.filter(aggregationModule -> !aggregationModule.getTargets().isEmpty())
.forEach(
aggregationModule -> {
IjModule module =
moduleFactory.createModule(
aggregationModule.getModuleBasePath(),
aggregationModule.getTargets(),
aggregationModule.getExcludes());
module
.getTargets()
.forEach(buildTarget -> moduleByBuildTarget.put(buildTarget, module));
});
return moduleByBuildTarget.build();
}
private static AggregationTree createAggregationTree(
AggregationModuleFactory aggregationModuleFactory,
ImmutableListMultimap<Path, TargetNode<?, ?>> targetNodesByBasePath) {
Map<Path, AggregationModule> pathToAggregationModuleMap =
targetNodesByBasePath
.asMap()
.entrySet()
.stream()
.collect(
MoreCollectors.toImmutableMap(
Map.Entry::getKey,
pathWithTargetNode ->
aggregationModuleFactory.createAggregationModule(
pathWithTargetNode.getKey(),
ImmutableSet.copyOf(pathWithTargetNode.getValue()))));
Path rootPath = Paths.get("");
AggregationModule rootAggregationModule = pathToAggregationModuleMap.get(rootPath);
if (rootAggregationModule == null) {
rootAggregationModule =
aggregationModuleFactory.createAggregationModule(rootPath, ImmutableSet.of());
}
AggregationTree aggregationTree = new AggregationTree(rootAggregationModule);
pathToAggregationModuleMap
.entrySet()
.stream()
.filter(e -> !rootPath.equals(e.getKey()))
.forEach(e -> aggregationTree.addModule(e.getKey(), e.getValue()));
return aggregationTree;
}
/**
* @param projectConfig the project config used
* @param targetGraph input graph.
* @param libraryFactory library factory.
* @param moduleFactory module factory.
* @return module graph corresponding to the supplied {@link TargetGraph}. Multiple targets from
* the same base path are mapped to a single module, therefore an IjModuleGraph edge exists
* between two modules (Ma, Mb) if a TargetGraph edge existed between a pair of nodes (Ta, Tb)
* and Ma contains Ta and Mb contains Tb.
*/
public static IjModuleGraph from(
final ProjectFilesystem projectFilesystem,
final IjProjectConfig projectConfig,
final TargetGraph targetGraph,
final IjLibraryFactory libraryFactory,
final IjModuleFactory moduleFactory,
final AggregationModuleFactory aggregationModuleFactory) {
ImmutableSet<String> ignoredTargetLabels = projectConfig.getIgnoredTargetLabels();
final ImmutableMap<BuildTarget, IjModule> rulesToModules =
createModules(
projectFilesystem,
targetGraph,
moduleFactory,
aggregationModuleFactory,
projectConfig.getAggregationMode().getGraphMinimumDepth(targetGraph.getNodes().size()),
ignoredTargetLabels);
final ExportedDepsClosureResolver exportedDepsClosureResolver =
new ExportedDepsClosureResolver(targetGraph, ignoredTargetLabels);
ImmutableMap.Builder<IjProjectElement, ImmutableMap<IjProjectElement, DependencyType>>
depsBuilder = ImmutableMap.builder();
final Set<IjLibrary> referencedLibraries = new HashSet<>();
for (final IjModule module : ImmutableSet.copyOf(rulesToModules.values())) {
Map<IjProjectElement, DependencyType> moduleDeps = new HashMap<>();
for (Map.Entry<BuildTarget, DependencyType> entry : module.getDependencies().entrySet()) {
BuildTarget depBuildTarget = entry.getKey();
TargetNode<?, ?> depTargetNode = targetGraph.get(depBuildTarget);
CommonDescriptionArg arg = (CommonDescriptionArg) depTargetNode.getConstructorArg();
if (arg.labelsContainsAnyOf(ignoredTargetLabels)) {
continue;
}
DependencyType depType = entry.getValue();
ImmutableSet<IjProjectElement> depElements;
if (depType.equals(DependencyType.COMPILED_SHADOW)) {
Optional<IjLibrary> library = libraryFactory.getLibrary(depTargetNode);
if (library.isPresent()) {
depElements = ImmutableSet.of(library.get());
} else {
depElements = ImmutableSet.of();
}
} else {
depElements =
Stream.concat(
exportedDepsClosureResolver.getExportedDepsClosure(depBuildTarget).stream(),
Stream.of(depBuildTarget))
.filter(
input -> {
TargetNode<?, ?> targetNode = targetGraph.get(input);
// IntelliJ doesn't support referring to source files which aren't below the root of
// the project. Filter out those cases proactively, so that we don't try to resolve
// files relative to the wrong ProjectFilesystem.
// Maybe one day someone will fix this.
return isInRootCell(projectFilesystem, targetNode);
})
.filter(
input -> {
// The exported deps closure can contain references back to targets contained
// in the module, so filter those out.
return !module.getTargets().contains(input);
})
.map(
depTarget -> {
IjModule depModule = rulesToModules.get(depTarget);
if (depModule != null) {
return depModule;
}
TargetNode<?, ?> targetNode = targetGraph.get(depTarget);
return libraryFactory.getLibrary(targetNode).orElse(null);
})
.filter(Objects::nonNull)
.collect(MoreCollectors.toImmutableSet());
}
for (IjProjectElement depElement : depElements) {
Preconditions.checkState(!depElement.equals(module));
DependencyType.putWithMerge(moduleDeps, depElement, depType);
}
}
if (!module.getExtraClassPathDependencies().isEmpty()) {
IjLibrary extraClassPathLibrary =
IjLibrary.builder()
.setClassPaths(module.getExtraClassPathDependencies())
.setTargets(ImmutableSet.of())
.setName("library_" + module.getName() + "_extra_classpath")
.build();
moduleDeps.put(extraClassPathLibrary, DependencyType.PROD);
}
moduleDeps
.keySet()
.stream()
.filter(dep -> dep instanceof IjLibrary)
.map(library -> (IjLibrary) library)
.forEach(referencedLibraries::add);
depsBuilder.put(module, ImmutableMap.copyOf(moduleDeps));
}
referencedLibraries.forEach(library -> depsBuilder.put(library, ImmutableMap.of()));
return new IjModuleGraph(depsBuilder.build());
}
private static boolean isInRootCell(
ProjectFilesystem projectFilesystem, TargetNode<?, ?> targetNode) {
return targetNode.getFilesystem().equals(projectFilesystem);
}
private IjModuleGraphFactory() {}
}