/*
* 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.model.DependencyType;
import com.facebook.buck.ide.intellij.model.IjModule;
import com.facebook.buck.ide.intellij.model.IjModuleAndroidFacet;
import com.facebook.buck.ide.intellij.model.IjModuleType;
import com.facebook.buck.ide.intellij.model.folders.IjFolder;
import com.facebook.buck.ide.intellij.model.folders.SourceFolder;
import com.facebook.buck.ide.intellij.model.folders.TestFolder;
import com.facebook.buck.model.BuildTarget;
import com.facebook.buck.rules.TargetNode;
import com.google.common.base.Preconditions;
import com.google.common.collect.HashMultimap;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Multimap;
import java.nio.file.Path;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
/** Holds all of the mutable state required during {@link IjModule} creation. */
public class ModuleBuildContext {
private final ImmutableSet<BuildTarget> circularDependencyInducingTargets;
private Optional<IjModuleAndroidFacet.Builder> androidFacetBuilder;
private ImmutableSet.Builder<Path> extraClassPathDependenciesBuilder;
private ImmutableSet.Builder<IjFolder> generatedSourceCodeFoldersBuilder;
private Map<Path, IjFolder> sourceFoldersMergeMap;
// See comment in getDependencies for these two member variables.
private Map<BuildTarget, DependencyType> dependencyTypeMap;
private Multimap<Path, BuildTarget> dependencyOriginMap;
private IjModuleType moduleType;
private Optional<Path> metaInfDirectory;
private Optional<String> javaLanguageLevel;
public ModuleBuildContext(ImmutableSet<BuildTarget> circularDependencyInducingTargets) {
this.circularDependencyInducingTargets = circularDependencyInducingTargets;
this.androidFacetBuilder = Optional.empty();
this.extraClassPathDependenciesBuilder = new ImmutableSet.Builder<>();
this.generatedSourceCodeFoldersBuilder = ImmutableSet.builder();
this.sourceFoldersMergeMap = new HashMap<>();
this.dependencyTypeMap = new HashMap<>();
this.dependencyOriginMap = HashMultimap.create();
this.moduleType = IjModuleType.UNKNOWN_MODULE;
this.metaInfDirectory = Optional.empty();
this.javaLanguageLevel = Optional.empty();
}
public void ensureAndroidFacetBuilder() {
if (!androidFacetBuilder.isPresent()) {
androidFacetBuilder = Optional.of(IjModuleAndroidFacet.builder());
}
}
public IjModuleAndroidFacet.Builder getOrCreateAndroidFacetBuilder() {
ensureAndroidFacetBuilder();
return androidFacetBuilder.get();
}
public boolean isAndroidFacetBuilderPresent() {
return androidFacetBuilder.isPresent();
}
public Optional<IjModuleAndroidFacet> getAndroidFacet() {
return androidFacetBuilder.map(IjModuleAndroidFacet.Builder::build);
}
public ImmutableSet<IjFolder> getSourceFolders() {
return ImmutableSet.copyOf(sourceFoldersMergeMap.values());
}
public void addExtraClassPathDependency(Path path) {
extraClassPathDependenciesBuilder.add(path);
}
public ImmutableSet<Path> getExtraClassPathDependencies() {
return extraClassPathDependenciesBuilder.build();
}
public void addGeneratedSourceCodeFolder(IjFolder generatedFolder) {
generatedSourceCodeFoldersBuilder.add(generatedFolder);
}
public ImmutableSet<IjFolder> getGeneratedSourceCodeFolders() {
return generatedSourceCodeFoldersBuilder.build();
}
public IjModuleType getModuleType() {
return moduleType;
}
public void setModuleType(IjModuleType moduleType) {
if (moduleType.hasHigherPriorityThan(this.moduleType)) {
this.moduleType = moduleType;
}
}
public Optional<Path> getMetaInfDirectory() {
return metaInfDirectory;
}
public void setMetaInfDirectory(Path metaInfDirectory) {
this.metaInfDirectory = Optional.of(metaInfDirectory);
}
public Optional<String> getJavaLanguageLevel() {
return javaLanguageLevel;
}
public void setJavaLanguageLevel(Optional<String> javaLanguageLevel) {
if (!this.javaLanguageLevel.isPresent()) {
this.javaLanguageLevel = javaLanguageLevel;
}
}
/**
* Adds a source folder to the context. If a folder with the same path has already been added the
* types of the two folders will be merged.
*
* @param folder folder to add/merge.
*/
public void addSourceFolder(IjFolder folder) {
Path path = folder.getPath();
IjFolder otherFolder = sourceFoldersMergeMap.get(path);
if (otherFolder != null) {
folder = mergeAllowingTestToBePromotedToSource(folder, otherFolder);
}
sourceFoldersMergeMap.put(path, folder);
}
private IjFolder mergeAllowingTestToBePromotedToSource(IjFolder from, IjFolder to) {
if ((from instanceof TestFolder && to instanceof SourceFolder)
|| (to instanceof TestFolder && from instanceof SourceFolder)) {
return new SourceFolder(
to.getPath(),
from.getWantsPackagePrefix() || to.getWantsPackagePrefix(),
IjFolder.combineInputs(from, to));
}
Preconditions.checkArgument(from.getClass() == to.getClass());
return from.merge(to);
}
public void addDeps(Iterable<BuildTarget> buildTargets, DependencyType dependencyType) {
addDeps(ImmutableSet.of(), buildTargets, dependencyType);
}
public void addCompileShadowDep(BuildTarget buildTarget) {
DependencyType.putWithMerge(dependencyTypeMap, buildTarget, DependencyType.COMPILED_SHADOW);
}
/**
* Record a dependency on a {@link BuildTarget}. The dependency's type will be merged if multiple
* {@link TargetNode}s refer to it or if multiple TargetNodes include sources from the same
* directory.
*
* @param sourcePaths the {@link Path}s to sources which need this dependency to build. Can be
* empty.
* @param buildTargets the {@link BuildTarget}s to depend on
* @param dependencyType what is the dependency needed for.
*/
public void addDeps(
ImmutableSet<Path> sourcePaths,
Iterable<BuildTarget> buildTargets,
DependencyType dependencyType) {
for (BuildTarget buildTarget : buildTargets) {
if (circularDependencyInducingTargets.contains(buildTarget)) {
continue;
}
if (sourcePaths.isEmpty()) {
DependencyType.putWithMerge(dependencyTypeMap, buildTarget, dependencyType);
} else {
for (Path sourcePath : sourcePaths) {
dependencyOriginMap.put(sourcePath, buildTarget);
}
}
}
}
public ImmutableMap<BuildTarget, DependencyType> getDependencies() {
// Some targets may introduce dependencies without contributing to the IjFolder set. These
// are recorded in the dependencyTypeMap.
// Dependencies associated with source paths inherit the type from the folder. This is because
// IntelliJ only operates on folders and so it is impossible to distinguish between test and
// production code if it's in the same folder. That in turn means test-only dependencies need
// to be "promoted" to production dependencies in the above scenario to keep code compiling.
// It is also possible that a target is included in both maps, in which case the type gets
// merged anyway.
// Merging types does not back-propagate: if TargetA depends on TargetB and the type of
// TargetB has been changed that does not mean the dependency type of TargetA is changed too.
Map<BuildTarget, DependencyType> result = new HashMap<>(dependencyTypeMap);
for (Path path : dependencyOriginMap.keySet()) {
DependencyType dependencyType =
Preconditions.checkNotNull(sourceFoldersMergeMap.get(path)) instanceof TestFolder
? DependencyType.TEST
: DependencyType.PROD;
for (BuildTarget buildTarget : dependencyOriginMap.get(path)) {
DependencyType.putWithMerge(result, buildTarget, dependencyType);
}
}
return ImmutableMap.copyOf(result);
}
}