/*
* Copyright 2013-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.io.ProjectFilesystem;
import com.facebook.buck.json.BuildFileParseException;
import com.facebook.buck.json.ProjectBuildFileParser;
import com.facebook.buck.rules.BuckPyFunction;
import com.facebook.buck.rules.coercer.DefaultTypeCoercerFactory;
import com.facebook.buck.util.Escaper;
import com.facebook.buck.util.HumanReadableException;
import com.facebook.buck.util.MoreStrings;
import com.facebook.buck.util.ObjectMappers;
import com.fasterxml.jackson.core.JsonGenerator;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Preconditions;
import com.google.common.base.Predicate;
import com.google.common.collect.FluentIterable;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import java.io.IOException;
import java.io.PrintStream;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.SortedSet;
import java.util.concurrent.atomic.AtomicLong;
import javax.annotation.Nullable;
import org.kohsuke.args4j.Argument;
import org.kohsuke.args4j.Option;
/**
* Evaluates a build file and prints out an equivalent build file with all includes/macros expanded.
* When complex macros are in play, this helps clarify what the resulting build rule definitions
* are.
*/
public class AuditRulesCommand extends AbstractCommand {
/** Indent to use in generated build file. */
private static final String INDENT = " ";
/** Properties that should be listed last in the declaration of a build rule. */
private static final ImmutableSet<String> LAST_PROPERTIES = ImmutableSet.of("deps", "visibility");
@Option(
name = "--type",
aliases = {"-t"},
usage = "The types of rule to filter by"
)
@Nullable
private List<String> types = null;
@Option(name = "--json", usage = "Print JSON representation of each rule")
private boolean json;
@Argument private List<String> arguments = new ArrayList<>();
public List<String> getArguments() {
return arguments;
}
public ImmutableSet<String> getTypes() {
return types == null ? ImmutableSet.of() : ImmutableSet.copyOf(types);
}
@Override
public String getShortDescription() {
return "List build rule definitions resulting from expanding macros.";
}
@Override
public int runWithoutHelp(CommandRunnerParams params) throws IOException, InterruptedException {
ProjectFilesystem projectFilesystem = params.getCell().getFilesystem();
try (ProjectBuildFileParser parser =
params
.getCell()
.createBuildFileParser(
new DefaultTypeCoercerFactory(), params.getConsole(), params.getBuckEventBus())) {
PrintStream out = params.getConsole().getStdOut();
for (String pathToBuildFile : getArguments()) {
if (!json) {
// Print a comment with the path to the build file.
out.printf("# %s\n\n", pathToBuildFile);
}
// Resolve the path specified by the user.
Path path = Paths.get(pathToBuildFile);
if (!path.isAbsolute()) {
Path root = projectFilesystem.getRootPath();
path = root.resolve(path);
}
// Parse the rules from the build file.
List<Map<String, Object>> rawRules;
try {
rawRules = parser.getAll(path, new AtomicLong());
} catch (BuildFileParseException e) {
throw new HumanReadableException(e);
}
// Format and print the rules from the raw data, filtered by type.
final ImmutableSet<String> types = getTypes();
Predicate<String> includeType = type -> types.isEmpty() || types.contains(type);
printRulesToStdout(params, rawRules, includeType);
}
} catch (BuildFileParseException e) {
throw new HumanReadableException("Unable to create parser");
}
return 0;
}
@Override
public boolean isReadOnly() {
return true;
}
private void printRulesToStdout(
CommandRunnerParams params,
List<Map<String, Object>> rawRules,
final Predicate<String> includeType)
throws IOException {
Iterable<Map<String, Object>> filteredRules =
FluentIterable.from(rawRules)
.filter(
rawRule -> {
String type = (String) rawRule.get(BuckPyFunction.TYPE_PROPERTY_NAME);
return includeType.apply(type);
});
PrintStream stdOut = params.getConsole().getStdOut();
if (json) {
Map<String, Object> rulesKeyedByName = new HashMap<>();
for (Map<String, Object> rawRule : filteredRules) {
String name = (String) rawRule.get("name");
Preconditions.checkNotNull(name);
rulesKeyedByName.put(name, Maps.filterValues(rawRule, v -> shouldInclude(v)));
}
// We create a new JsonGenerator that does not close the stream.
try (JsonGenerator generator =
ObjectMappers.createGenerator(stdOut)
.disable(JsonGenerator.Feature.AUTO_CLOSE_TARGET)
.useDefaultPrettyPrinter()) {
ObjectMappers.WRITER.writeValue(generator, rulesKeyedByName);
}
stdOut.print('\n');
} else {
for (Map<String, Object> rawRule : filteredRules) {
printRuleAsPythonToStdout(stdOut, rawRule);
}
}
}
private void printRuleAsPythonToStdout(PrintStream out, Map<String, Object> rawRule) {
String type = (String) rawRule.get(BuckPyFunction.TYPE_PROPERTY_NAME);
out.printf("%s(\n", type);
// The properties in the order they should be displayed for this rule.
LinkedHashSet<String> properties = Sets.newLinkedHashSet();
// Always display the "name" property first.
properties.add("name");
// Add the properties specific to the rule.
SortedSet<String> customProperties = Sets.newTreeSet();
for (String key : rawRule.keySet()) {
// Ignore keys that start with "buck.".
if (!(key.startsWith(BuckPyFunction.INTERNAL_PROPERTY_NAME_PREFIX)
|| LAST_PROPERTIES.contains(key))) {
customProperties.add(key);
}
}
properties.addAll(customProperties);
// Add common properties that should be displayed last.
properties.addAll(Sets.intersection(LAST_PROPERTIES, rawRule.keySet()));
// Write out the properties and their corresponding values.
for (String property : properties) {
Object rawValue = rawRule.get(property);
if (!shouldInclude(rawValue)) {
continue;
}
String displayValue = createDisplayString(INDENT, rawValue);
out.printf("%s%s = %s,\n", INDENT, property, displayValue);
}
// Close the rule definition.
out.printf(")\n\n");
}
private boolean shouldInclude(@Nullable Object rawValue) {
return rawValue != null
&& rawValue != Optional.empty()
&& !(rawValue instanceof Collection && ((Collection<?>) rawValue).isEmpty());
}
/**
* @param value in a Map returned by {@link ProjectBuildFileParser#getAll(Path, AtomicLong)}.
* @return a string that represents the Python equivalent of the value.
*/
@VisibleForTesting
static String createDisplayString(@Nullable Object value) {
return createDisplayString("", value);
}
static String createDisplayString(String indent, @Nullable Object value) {
if (value == null) {
return "None";
} else if (value instanceof Boolean) {
return MoreStrings.capitalize(value.toString());
} else if (value instanceof String) {
return Escaper.escapeAsPythonString(value.toString());
} else if (value instanceof Number) {
return value.toString();
} else if (value instanceof List) {
StringBuilder out = new StringBuilder("[\n");
String indentPlus1 = indent + INDENT;
for (Object item : (List<?>) value) {
out.append(indentPlus1).append(createDisplayString(indentPlus1, item)).append(",\n");
}
out.append(indent).append("]");
return out.toString();
} else if (value instanceof Map) {
StringBuilder out = new StringBuilder("{\n");
String indentPlus1 = indent + INDENT;
for (Map.Entry<?, ?> entry : ((Map<?, ?>) value).entrySet()) {
out.append(indentPlus1)
.append(createDisplayString(indentPlus1, entry.getKey()))
.append(": ")
.append(createDisplayString(indentPlus1, entry.getValue()))
.append(",\n");
}
out.append(indent).append("}");
return out.toString();
} else {
throw new IllegalStateException();
}
}
}