/* * Copyright 2016-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.cli; import com.facebook.buck.graph.MutableDirectedGraph; import com.facebook.buck.io.ProjectFilesystem; import com.facebook.buck.jvm.java.JavaFileParser; import com.facebook.buck.jvm.java.JavaLibraryDescription; import com.facebook.buck.jvm.java.autodeps.JavaDepsFinder; import com.facebook.buck.model.BuildTarget; import com.facebook.buck.rules.PathSourcePath; import com.facebook.buck.rules.SourcePath; import com.facebook.buck.rules.TargetGraph; import com.facebook.buck.rules.TargetNode; import com.facebook.buck.rules.VisibilityPattern; import com.facebook.buck.util.Console; import com.google.common.base.Joiner; import com.google.common.base.Preconditions; import com.google.common.collect.FluentIterable; import com.google.common.collect.HashMultimap; import com.google.common.collect.Iterables; import com.google.common.collect.Multimap; import com.google.common.collect.Ordering; import java.nio.file.Path; import java.util.Comparator; import java.util.HashMap; import java.util.Map; import java.util.Optional; import java.util.Set; import java.util.SortedSet; import java.util.TreeMap; import java.util.TreeSet; /** * Tool that implements the bulk of the work for {@code buck suggest}. For a given build target in * the target graph, it will divide its {@code srcs} into strongly connected components and use * those to suggest a new set of build rule definitions with maximally fine-grained dependencies. * * <p>Note that because this is a tool that is trying to provide information about the user's * dependencies, it generally favors printing errors to stderr rather than throwing exceptions and * halting. As a tool, it would be less useful if it did not provide any information until the user * cleaned up all of his or her code. The user is likely running {@code buck suggest} to enable them * to clean things up. */ class FineGrainedJavaDependencySuggester { private final BuildTarget suggestedTarget; private final TargetGraph graph; private final JavaDepsFinder javaDepsFinder; private final Console console; FineGrainedJavaDependencySuggester( BuildTarget suggestedTarget, TargetGraph graph, JavaDepsFinder javaDepsFinder, Console console) { this.suggestedTarget = suggestedTarget; this.graph = graph; this.javaDepsFinder = javaDepsFinder; this.console = console; } /** * Suggests a refactoring by printing it to stdout (with warnings printed to stderr). * * @throws IllegalArgumentException */ void suggestRefactoring() { final TargetNode<?, ?> suggestedNode = graph.get(suggestedTarget); if (!(suggestedNode.getConstructorArg() instanceof JavaLibraryDescription.CoreArg)) { console.printErrorText( String.format("'%s' does not correspond to a Java rule", suggestedTarget)); throw new IllegalArgumentException(); } JavaLibraryDescription.CoreArg arg = (JavaLibraryDescription.CoreArg) suggestedNode.getConstructorArg(); JavaFileParser javaFileParser = javaDepsFinder.getJavaFileParser(); Multimap<String, String> providedSymbolToRequiredSymbols = HashMultimap.create(); Map<String, PathSourcePath> providedSymbolToSrc = new HashMap<>(); for (SourcePath src : arg.getSrcs()) { extractProvidedSymbolInfoFromSourceFile( src, javaFileParser, providedSymbolToRequiredSymbols, providedSymbolToSrc); } // Create a MutableDirectedGraph from the providedSymbolToRequiredSymbols. MutableDirectedGraph<String> symbolsDependencies = new MutableDirectedGraph<>(); // Iterate the keys of providedSymbolToSrc rather than providedSymbolToRequiredSymbols because // providedSymbolToRequiredSymbols will not have any entries for providedSymbols with no // dependencies. for (String providedSymbol : providedSymbolToSrc.keySet()) { // Add a node for the providedSymbol in case it has no edges. symbolsDependencies.addNode(providedSymbol); for (String requiredSymbol : providedSymbolToRequiredSymbols.get(providedSymbol)) { if (providedSymbolToRequiredSymbols.containsKey(requiredSymbol) && !providedSymbol.equals(requiredSymbol)) { symbolsDependencies.addEdge(providedSymbol, requiredSymbol); } } } // Determine the strongly connected components. Set<Set<String>> stronglyConnectedComponents = symbolsDependencies.findStronglyConnectedComponents(); // Maps a providedSymbol to the component that contains it. Map<String, NamedStronglyConnectedComponent> namedComponentsIndex = new TreeMap<>(); Set<NamedStronglyConnectedComponent> namedComponents = new TreeSet<>(); for (Set<String> stronglyConnectedComponent : stronglyConnectedComponents) { // We just use the first provided symbol in the strongly connected component as the canonical // name for the component. Maybe not the best name, but certainly not the worst. String name = Iterables.getFirst(stronglyConnectedComponent, /* defaultValue */ null); if (name == null) { throw new IllegalStateException( "A strongly connected component was created with zero nodes."); } NamedStronglyConnectedComponent namedComponent = new NamedStronglyConnectedComponent(name, stronglyConnectedComponent); namedComponents.add(namedComponent); for (String providedSymbol : stronglyConnectedComponent) { namedComponentsIndex.put(providedSymbol, namedComponent); } } // Visibility argument. StringBuilder visibilityBuilder = new StringBuilder(" visibility = [\n"); SortedSet<String> visibilities = FluentIterable.from(suggestedNode.getVisibilityPatterns()) .transform(VisibilityPattern::getRepresentation) .toSortedSet(Ordering.natural()); for (String visibility : visibilities) { visibilityBuilder.append(" '" + visibility + "',\n"); } visibilityBuilder.append(" ],\n"); String visibilityArg = visibilityBuilder.toString(); // Print out the new version of the original rule. console .getStdOut() .printf( "java_library(\n" + " name = '%s',\n" + " exported_deps = [\n", suggestedTarget.getShortName()); for (NamedStronglyConnectedComponent namedComponent : namedComponents) { console.getStdOut().printf(" ':%s',\n", namedComponent.name); } console.getStdOut().print(" ],\n" + visibilityArg + ")\n"); // Print out a rule for each of the strongly connected components. JavaDepsFinder.DependencyInfo dependencyInfo = javaDepsFinder.findDependencyInfoForGraph(graph); for (NamedStronglyConnectedComponent namedComponent : namedComponents) { String buildRuleDefinition = createBuildRuleDefinition( namedComponent, providedSymbolToSrc, providedSymbolToRequiredSymbols, namedComponentsIndex, dependencyInfo, symbolsDependencies, visibilityArg); console.getStdOut().print(buildRuleDefinition); } } /** Extracts the features from {@code src} and updates the collections accordingly. */ private void extractProvidedSymbolInfoFromSourceFile( SourcePath src, JavaFileParser javaFileParser, Multimap<String, String> providedSymbolToRequiredSymbols, Map<String, PathSourcePath> providedSymbolToSrc) { if (!(src instanceof PathSourcePath)) { return; } PathSourcePath path = (PathSourcePath) src; ProjectFilesystem filesystem = path.getFilesystem(); Optional<String> contents = filesystem.readFileIfItExists(path.getRelativePath()); if (!contents.isPresent()) { throw new RuntimeException(String.format("Could not read file '%s'", path.getRelativePath())); } JavaFileParser.JavaFileFeatures features = javaFileParser.extractFeaturesFromJavaCode(contents.get()); // If there are multiple provided symbols, that is because there are inner classes. Choosing // the shortest name will effectively select the top-level type. String providedSymbol = Iterables.getFirst(features.providedSymbols, /* defaultValue */ null); if (providedSymbol == null) { console .getStdErr() .printf("%s cowardly refuses to provide any types.\n", path.getRelativePath()); return; } providedSymbolToSrc.put(providedSymbol, path); providedSymbolToRequiredSymbols.putAll(providedSymbol, features.requiredSymbols); providedSymbolToRequiredSymbols.putAll(providedSymbol, features.exportedSymbols); } /** Creates the build rule definition for the {@code namedComponent}. */ private String createBuildRuleDefinition( NamedStronglyConnectedComponent namedComponent, Map<String, PathSourcePath> providedSymbolToSrc, Multimap<String, String> providedSymbolToRequiredSymbols, Map<String, NamedStronglyConnectedComponent> namedComponentsIndex, JavaDepsFinder.DependencyInfo dependencyInfo, MutableDirectedGraph<String> symbolsDependencies, String visibilityArg) { final TargetNode<?, ?> suggestedNode = graph.get(suggestedTarget); SortedSet<String> deps = new TreeSet<>(LOCAL_DEPS_FIRST_COMPARATOR); SortedSet<PathSourcePath> srcs = new TreeSet<>(); for (String providedSymbol : namedComponent.symbols) { PathSourcePath src = providedSymbolToSrc.get(providedSymbol); srcs.add(src); for (String requiredSymbol : providedSymbolToRequiredSymbols.get(providedSymbol)) { // First, check to see whether the requiredSymbol is in one of the newly created // strongly connected components. If so, add it to the deps so long as it is not the // strongly connected component that we are currently exploring. NamedStronglyConnectedComponent requiredComponent = namedComponentsIndex.get(requiredSymbol); if (requiredComponent != null) { if (!requiredComponent.equals(namedComponent)) { deps.add(":" + requiredComponent.name); } continue; } Set<TargetNode<?, ?>> depProviders = dependencyInfo.symbolToProviders.get(requiredSymbol); if (depProviders == null || depProviders.size() == 0) { console.getStdErr().printf("# Suspicious: no provider for '%s'\n", requiredSymbol); continue; } depProviders = FluentIterable.from(depProviders) .filter(provider -> provider.isVisibleTo(suggestedNode)) .toSet(); TargetNode<?, ?> depProvider; if (depProviders.size() == 1) { depProvider = Iterables.getOnlyElement(depProviders); } else { console .getStdErr() .printf( "# Suspicious: no lone provider for '%s': [%s]\n", requiredSymbol, Joiner.on(", ").join(depProviders)); continue; } if (!depProvider.equals(suggestedNode)) { deps.add(depProvider.toString()); } } // Find deps within package. for (String requiredSymbol : symbolsDependencies.getOutgoingNodesFor(providedSymbol)) { NamedStronglyConnectedComponent componentDep = Preconditions.checkNotNull(namedComponentsIndex.get(requiredSymbol)); if (!componentDep.equals(namedComponent)) { deps.add(":" + componentDep.name); } } } final Path basePathForSuggestedTarget = suggestedTarget.getBasePath(); Iterable<String> relativeSrcs = FluentIterable.from(srcs) .transform( input -> basePathForSuggestedTarget.relativize(input.getRelativePath()).toString()); StringBuilder rule = new StringBuilder( "\njava_library(\n" + " name = '" + namedComponent.name + "',\n" + " srcs = [\n"); for (String src : relativeSrcs) { rule.append(String.format(" '%s',\n", src)); } rule.append(" ],\n" + " deps = [\n"); for (String dep : deps) { rule.append(String.format(" '%s',\n", dep)); } rule.append(" ],\n" + visibilityArg + ")\n"); return rule.toString(); } /** * A strongly connected component is going to become a java_library() rule. These components have * dependencies on one another, so it's important to be able to determine their names so they can * be listed in the deps. */ private static class NamedStronglyConnectedComponent implements Comparable<NamedStronglyConnectedComponent> { private final String name; private final Set<String> symbols; NamedStronglyConnectedComponent(String name, Set<String> symbols) { this.name = name; this.symbols = symbols; } @Override public int compareTo(NamedStronglyConnectedComponent that) { return this.name.compareTo(that.name); } } private static final Comparator<String> LOCAL_DEPS_FIRST_COMPARATOR = (dep1, dep2) -> { boolean isDep1Local = dep1.startsWith(":"); boolean isDep2Local = dep2.startsWith(":"); if (isDep1Local == isDep2Local) { return dep1.compareTo(dep2); } else if (isDep1Local) { return -1; } else { return 1; } }; }