blob: cca7f24a3e8a82ae6a36a246c6a6c0b93154422e [file] [log] [blame]
/*
* Copyright 2014-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.java;
import com.facebook.buck.android.AndroidLibraryDescription;
import com.facebook.buck.cli.BuckConfig;
import com.facebook.buck.event.BuckEventBus;
import com.facebook.buck.event.ThrowableConsoleEvent;
import com.facebook.buck.io.ProjectFilesystem;
import com.facebook.buck.json.BuildFileParseException;
import com.facebook.buck.json.ProjectBuildFileParser;
import com.facebook.buck.json.ProjectBuildFileParserFactory;
import com.facebook.buck.model.BuildTarget;
import com.facebook.buck.parser.ParserConfig;
import com.facebook.buck.util.Console;
import com.google.common.base.Preconditions;
import com.google.common.collect.HashMultimap;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableMultimap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.ImmutableSetMultimap;
import com.google.common.collect.ImmutableSortedSet;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.Multimap;
import java.io.IOException;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* This class is responsible for finding Java source files and the buck rules that own them, given a
* fully-qualified Java symbol like "com.example.foo.Bar". It does this by looking at expected
* locations based on the package name of the symbol and the source roots listed in the project
* config. This functionality is used to automatically generate dependency information.
*/
public class JavaSymbolFinder {
private static ImmutableSet<String> javaRuleTypes = ImmutableSet.of(
JavaLibraryDescription.TYPE.getName(),
AndroidLibraryDescription.TYPE.getName(),
JavaTestDescription.TYPE.getName());
private final ProjectFilesystem projectFilesystem;
private final SrcRootsFinder srcRootsFinder;
private final JavacOptions javacOptions;
private final ProjectBuildFileParserFactory projectBuildFileParserFactory;
private final BuckConfig config;
private final BuckEventBus buckEventBus;
private final Console console;
private final ImmutableMap<String, String> environment;
public JavaSymbolFinder(
ProjectFilesystem projectFilesystem,
SrcRootsFinder srcRootsFinder,
JavacOptions javacOptions,
ProjectBuildFileParserFactory projectBuildFileParserFactory,
BuckConfig config,
BuckEventBus buckEventBus,
Console console,
ImmutableMap<String, String> environment) {
this.projectFilesystem = projectFilesystem;
this.srcRootsFinder = srcRootsFinder;
this.javacOptions = javacOptions;
this.projectBuildFileParserFactory = projectBuildFileParserFactory;
this.config = config;
this.buckEventBus = buckEventBus;
this.console = console;
this.environment = environment;
}
/**
* Figure out the build targets that provide a set of Java symbols.
* @param symbols The set of symbols (e.g. "com.example.foo.Bar") to find defining targets for.
* This is taken as a collection, rather than as an individual string, because
* instantiating a ProjectBuildFileParser is expensive (it spawns a Python
* subprocess), and we don't want to encourage the caller to do it more than once.
* @return A multimap of symbols to the targets that define them, of the form:
* {"com.example.a.A": set("//com/example/a:a", "//com/another/a:a")}
*/
public ImmutableSetMultimap<String, BuildTarget> findTargetsForSymbols(Set<String> symbols)
throws InterruptedException {
// TODO(jacko): Handle files that aren't included in any rule.
// First find all the source roots in the current project.
Collection<Path> srcRoots;
try {
srcRoots = srcRootsFinder.getAllSrcRootPaths(config.getSrcRoots());
} catch (IOException e) {
buckEventBus.post(ThrowableConsoleEvent.create(e, "Error while searching for source roots."));
return ImmutableSetMultimap.of();
}
// Now collect all the code files that define our symbols.
Multimap<String, Path> symbolsToSourceFiles = HashMultimap.create();
for (String symbol : symbols) {
symbolsToSourceFiles.putAll(symbol, getDefiningPaths(symbol, srcRoots));
}
// Now find all the targets that define all those code files. We do this in one pass because we
// don't want to instantiate a new parser subprocess for every symbol.
Set<Path> allSourceFilePaths = ImmutableSet.copyOf(symbolsToSourceFiles.values());
Multimap<Path, BuildTarget> sourceFilesToTargets = getTargetsForSourceFiles(allSourceFilePaths);
// Now build the map from from symbols to build targets.
ImmutableSetMultimap.Builder<String, BuildTarget> symbolsToTargets =
ImmutableSetMultimap.builder();
for (String symbol : symbolsToSourceFiles.keySet()) {
for (Path sourceFile : symbolsToSourceFiles.get(symbol)) {
symbolsToTargets.putAll(symbol, sourceFilesToTargets.get(sourceFile));
}
}
return symbolsToTargets.build();
}
/**
* For all the possible BUCK files above each of the given source files, parse them to JSON to
* find the targets that actually include these source files, and return a map of them. We do this
* over a collection of source files, rather than a single file at a time, because instantiating
* the BUCK file parser is expensive. (It spawns a Python subprocess.)
*/
private ImmutableMultimap<Path, BuildTarget> getTargetsForSourceFiles(
Collection<Path> sourceFilePaths) throws InterruptedException {
Map<Path, List<Map<String, Object>>> parsedBuildFiles = Maps.newHashMap();
ImmutableSetMultimap.Builder<Path, BuildTarget> sourceFileTargetsMultimap =
ImmutableSetMultimap.builder();
try (ProjectBuildFileParser parser = projectBuildFileParserFactory.createParser(
console,
environment,
buckEventBus)) {
for (Path sourceFile : sourceFilePaths) {
for (Path buckFile : possibleBuckFilesForSourceFile(sourceFile)) {
List<Map<String, Object>> rules;
// Avoid parsing the same BUCK file twice.
if (parsedBuildFiles.containsKey(buckFile)) {
rules = parsedBuildFiles.get(buckFile);
} else {
rules = parser.getAll(buckFile);
parsedBuildFiles.put(buckFile, rules);
}
for (Map<String, Object> ruleMap : rules) {
String type = (String) ruleMap.get("type");
if (javaRuleTypes.contains(type)) {
@SuppressWarnings("unchecked")
List<String> srcs = (List<String>) Preconditions.checkNotNull(ruleMap.get("srcs"));
if (isSourceFilePathInSrcsList(sourceFile, srcs, buckFile.getParent())) {
Path buckFileDir = buckFile.getParent();
String baseName = "//" + (buckFileDir != null ? buckFileDir : "");
String shortName = (String) ruleMap.get("name");
sourceFileTargetsMultimap.put(
sourceFile,
BuildTarget.builder(baseName, shortName).build());
}
}
}
}
}
} catch (BuildFileParseException e) {
buckEventBus.post(ThrowableConsoleEvent.create(e, "Error while searching for targets."));
}
return sourceFileTargetsMultimap.build();
}
/**
* The "srcs" list of a rule is given relative to the path of the BUCK file. Resolve and normalize
* these paths to see if a given source file (given relative to the project root) is among them.
*/
private boolean isSourceFilePathInSrcsList(
Path candidateFilePath,
Collection<String> srcs,
Path srcsDir) {
Path normalizedCandidatePath = candidateFilePath.normalize();
for (String src : srcs) {
Path pathForSrc = Paths.get(src).normalize();
Path projectRelativePathForSrc = (srcsDir != null ? srcsDir.resolve(pathForSrc) : pathForSrc);
Path normalizedPathForSrc = projectRelativePathForSrc.normalize();
if (normalizedCandidatePath.equals(normalizedPathForSrc)) {
return true;
}
}
return false;
}
/**
* Look at all the directories above a given source file, up to the project root, and return the
* paths to any BUCK files that exist at those locations. These files are the only ones that could
* define a rule that includes the given source file.
*/
private ImmutableList<Path> possibleBuckFilesForSourceFile(Path sourceFilePath) {
ImmutableList.Builder<Path> possibleBuckFiles = ImmutableList.builder();
Path dir = sourceFilePath.getParent();
ParserConfig parserConfig = new ParserConfig(config);
// For a source file like foo/bar/example.java, add paths like foo/bar/BUCK and foo/BUCK.
while (dir != null) {
Path buckFile = dir.resolve(parserConfig.getBuildFileName());
if (projectFilesystem.isFile(buckFile)) {
possibleBuckFiles.add(buckFile);
}
dir = dir.getParent();
}
// Finally, add ./BUCK in the root directory.
Path rootBuckFile = Paths.get(parserConfig.getBuildFileName());
if (projectFilesystem.exists(rootBuckFile)) {
possibleBuckFiles.add(rootBuckFile);
}
return possibleBuckFiles.build();
}
/**
* Find all Java source files that define a given fully-qualified symbol (like "com.example.a.A").
* To do this, open up all the Java files that could define it (see {@link #getCandidatePaths})
* and parse them with our Eclipse-based {@link JavaFileParser}.
*/
private ImmutableSortedSet<Path> getDefiningPaths(String symbol, Collection<Path> srcRoots) {
ImmutableSortedSet.Builder<Path> definingPaths = ImmutableSortedSet.naturalOrder();
// TODO(simons): This should use the same javac env as was used for compiling the code.
JavaFileParser parser = JavaFileParser.createJavaFileParser(javacOptions);
for (Path candidatePath : getCandidatePaths(symbol, srcRoots)) {
try {
String content = projectFilesystem.readFileIfItExists(
projectFilesystem.getPathForRelativeExistingPath(candidatePath)).get();
Set<String> symbols = parser.getExportedSymbolsFromString(content);
if (symbols.contains(symbol)) {
definingPaths.add(candidatePath);
}
} catch (IOException e) {
buckEventBus.post(
ThrowableConsoleEvent.create(e, "Error while searching for source files."));
}
}
return definingPaths.build();
}
/**
* Guessing file names from fully-qualified Java symbols is ambiguous, because we don't know ahead
* of time exactly what part of the symbol is the package, and what part is class names or static
* members. This returns the set of all possible Java files for a given symbol, given different
* possibilities for the package name and resolving against all the available source roots.
* Returns only those candidates that actually exist.
*/
private ImmutableSortedSet<Path> getCandidatePaths(String symbol, Collection<Path> srcRoots) {
ImmutableSortedSet.Builder<Path> candidatePaths = ImmutableSortedSet.naturalOrder();
List<String> symbolParts = Lists.newArrayList(symbol.split("\\."));
for (int symbolIndex = 0; symbolIndex < symbolParts.size(); symbolIndex++) {
List<String> pathPartsList = symbolParts.subList(0, symbolIndex);
String[] pathParts = pathPartsList.toArray(new String[pathPartsList.size()]);
String candidateFileName = symbolParts.get(symbolIndex) + ".java";
for (Path srcRoot : srcRoots) {
Path candidatePath = srcRoot.resolve(Paths.get("", pathParts)).resolve(candidateFileName);
if (projectFilesystem.exists(candidatePath)) {
candidatePaths.add(candidatePath);
}
}
}
return candidatePaths.build();
}
}