com.facebook.buck.jvm.java.JavaSymbolFinder.java Source code

Java tutorial

Introduction

Here is the source code for com.facebook.buck.jvm.java.JavaSymbolFinder.java

Source

/*
 * 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.jvm.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.MorePaths;
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.rules.BuckPyFunction;
import com.facebook.buck.rules.ConstructorArgMarshaller;
import com.facebook.buck.rules.Description;
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.Optional;
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(
            Description.getBuildRuleType(JavaLibraryDescription.class).getName(),
            Description.getBuildRuleType(AndroidLibraryDescription.class).getName(),
            Description.getBuildRuleType(JavaTestDescription.class).getName());

    private final ProjectFilesystem projectFilesystem;
    private final SrcRootsFinder srcRootsFinder;
    private final JavacOptions javacOptions;
    private final ConstructorArgMarshaller marshaller;
    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, ConstructorArgMarshaller marshaller,
            ProjectBuildFileParserFactory projectBuildFileParserFactory, BuckConfig config,
            BuckEventBus buckEventBus, Console console, ImmutableMap<String, String> environment) {
        this.projectFilesystem = projectFilesystem;
        this.srcRootsFinder = srcRootsFinder;
        this.javacOptions = javacOptions;
        this.marshaller = marshaller;
        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, IOException {
        // TODO(oconnor663): 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.getView(JavaBuckConfig.class).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, IOException {
        Map<Path, List<Map<String, Object>>> parsedBuildFiles = Maps.newHashMap();
        ImmutableSetMultimap.Builder<Path, BuildTarget> sourceFileTargetsMultimap = ImmutableSetMultimap.builder();
        try (ProjectBuildFileParser parser = projectBuildFileParserFactory.createParser(marshaller, console,
                environment, buckEventBus, /* ignoreBuckAutodepsFiles */ false)) {
            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(BuckPyFunction.TYPE_PROPERTY_NAME);
                        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 ? MorePaths.pathWithUnixSeparators(buckFileDir)
                                                : "");
                                String shortName = (String) Preconditions.checkNotNull(ruleMap.get("name"));
                                sourceFileTargetsMultimap.put(sourceFile, BuildTarget
                                        .builder(projectFilesystem.getRootPath(), 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 = config.getView(ParserConfig.class);

        // 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(shs96c): 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)) {
            Optional<String> content = projectFilesystem
                    .readFileIfItExists(projectFilesystem.getPathForRelativeExistingPath(candidatePath));
            if (content.isPresent() && parser.getExportedSymbolsFromString(content.get()).contains(symbol)) {
                definingPaths.add(candidatePath);
            }
        }
        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();
    }
}