Add a class that can generate a subset of buck.py and populate constructor args reflectively.
Summary:
As part of the work to add a plugin mechanism, we need a way of allowing new extensions to declare
what arguments and dependencies they have without needing to modify buck.py. The
BuildableArgInspector provides that.
Test Plan: buck test --all
diff --git a/src/com/facebook/buck/rules/ArgObjectPopulatomatic.java b/src/com/facebook/buck/rules/ArgObjectPopulatomatic.java
new file mode 100644
index 0000000..073b914
--- /dev/null
+++ b/src/com/facebook/buck/rules/ArgObjectPopulatomatic.java
@@ -0,0 +1,130 @@
+/*
+ * 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.rules;
+
+import com.facebook.buck.model.BuildTarget;
+import com.google.common.base.Optional;
+import com.google.common.base.Preconditions;
+import com.google.common.collect.ImmutableSet;
+
+import java.lang.reflect.Field;
+import java.lang.reflect.Modifier;
+import java.nio.file.Path;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * Used to derive information from the constructor args returned by {@link Description} instances.
+ * There are two major uses this information is put to: populating the DTO object from the
+ * {@link BuildRuleFactoryParams}, which is populated by the functions added to Buck's core build
+ * file parsing script. The second function of this class is to generate those functions.
+ * <p>
+ * The constructor arg is examined for public fields and writable properties, as defined by the
+ * JavaBean specification. Reflection is used to determine the type of the field, and it's possible
+ * to indicate that a field isn't mandatory by using {@link Optional} declarations. If the name of
+ * the property to be exposed to a build file does not match the field/bean property name, then a
+ * {@link Hint} can be used to set it.
+ * <p>
+ * As an example, the arguments for a build target defined as:
+ * <pre>
+ * example_library(
+ * name = 'example',
+ * srcs = [ '//foo:bar', 'Eggs.java', ],
+ * optional_thing = True,
+ * )
+ * </pre>
+ * Could be defined as a constructor arg that looks like:
+ * <pre>
+ * public class ExampleLibraryArg {
+ * public String name;
+ * public ImmutableSortedSet>SourcePath> srcs;
+ * @Hint(name = "optional_thing")
+ * public Optional>Boolean> isNeeded;
+ * }
+ * </pre>
+ */
+// TODO(simons): Revisit class name when we add Descriptions
+public class ArgObjectPopulatomatic {
+
+ private final Path basePath;
+
+ /**
+ * Constructor. {@code pathFromProjectRootToBuildFile} is the path relative to the project root to
+ * the build file that has called the build rule's function in buck.py. This is used for resolving
+ * additional paths to ones relative to the project root, and to allow {@link BuildTarget}
+ * instances to be fully qualified.
+ *
+ * @param pathFromProjectRootToBuildFile The path from the root of the project to the directory of
+ * the build file that this function is being created from.
+ */
+ public ArgObjectPopulatomatic(Path pathFromProjectRootToBuildFile) {
+ Preconditions.checkNotNull(pathFromProjectRootToBuildFile);
+
+ // Without this check an IndexOutOfBounds exception is thrown by normalize.
+ if (pathFromProjectRootToBuildFile.toString().isEmpty()) {
+ this.basePath = pathFromProjectRootToBuildFile;
+ } else {
+ this.basePath = pathFromProjectRootToBuildFile.normalize();
+ }
+ }
+
+ /**
+ * Use the information contained in the {@code params} to fill in the public fields and settable
+ * properties of {@code dto}. The following rules are used:
+ * <ul>
+ * <li>Boolean values are set to true or false.</li>
+ * <li>{@link BuildRule}s will be resovled.</li>
+ * <li>{@link BuildTarget}s are resolved and will be fully qualified.</li>
+ * <li>Numeric values are handled as if being cast from Long.</li>
+ * <li>{@link SourcePath} instances will be set to the appropriate implementation.</li>
+ * <li>{@link Path} declarations will be set to be relative to the project root.</li>
+ * <li>Strings will be set "as is".</li>
+ * <li>{@link List}s and {@link Set}s will be populated with the expected generic type,
+ * provided the wildcarding allows an upperbound to be determined to be one of the above.</li>
+ * </ul>
+ *
+ * Any property that is marked as being an {@link Optional} field will be set to a default value
+ * if none is set. This is typically {@link Optional#absent()}, but in the case of collections is
+ * an empty collection.
+ *
+ * @param ruleResolver The resolver to use when looking up {@link BuildRule}s.
+ * @param params The parameters to be used to populate the {@code dto} instance.
+ * @param dto The constructor dto to be populated.
+ */
+ public void populate(BuildRuleResolver ruleResolver, BuildRuleFactoryParams params, Object dto) {
+ Set<ParamInfo> allInfo = getAllParamInfo(dto);
+
+ for (ParamInfo info : allInfo) {
+ info.setFromParams(ruleResolver, dto, params);
+ }
+ }
+
+ ImmutableSet<ParamInfo> getAllParamInfo(Object dto) {
+ Class<?> argClass = dto.getClass();
+
+ ImmutableSet.Builder<ParamInfo> allInfo = ImmutableSet.builder();
+
+ for (Field field : argClass.getFields()) {
+ if (Modifier.isFinal(field.getModifiers())) {
+ continue;
+ }
+ allInfo.add(new ParamInfo(basePath, field));
+ }
+
+ return allInfo.build();
+ }
+}
diff --git a/src/com/facebook/buck/rules/BUCK b/src/com/facebook/buck/rules/BUCK
index a92ba22..2d2026d 100644
--- a/src/com/facebook/buck/rules/BUCK
+++ b/src/com/facebook/buck/rules/BUCK
@@ -32,8 +32,10 @@
'AbstractBuildRuleBuilderParams.java',
'AbstractSourcePath.java',
'AnnotationProcessingData.java',
+ 'ArgObjectPopulatomatic.java',
'ArtifactCache.java',
'BinaryBuildRule.java',
+ 'BuckPyFunction.java',
'Buildable.java',
'Buildables.java',
'BuildableContext.java',
@@ -55,8 +57,10 @@
'DefaultBuildRuleBuilderParams.java',
'DependencyGraph.java',
'FileSourcePath.java',
+ 'Hint.java',
'JavaPackageFinder.java',
'OnDiskBuildInfo.java',
+ 'ParamInfo.java',
'RuleKey.java',
'RuleKeyBuilderFactory.java',
'Sha1HashCode.java',
@@ -140,12 +144,12 @@
'//src/com/facebook/buck/step:step',
'//src/com/facebook/buck/step/fs:fs',
'//src/com/facebook/buck/test:test',
+ '//src/com/facebook/buck/timing:timing',
'//src/com/facebook/buck/util:constants',
'//src/com/facebook/buck/util/collect:collect',
'//src/com/facebook/buck/util:exceptions',
'//src/com/facebook/buck/util:io',
'//src/com/facebook/buck/util:network',
- '//src/com/facebook/buck/timing:timing',
'//src/com/facebook/buck/util:util',
'//src/com/facebook/buck/util/concurrent:concurrent',
'//src/com/facebook/buck/util/environment:environment',
diff --git a/src/com/facebook/buck/rules/BuckPyFunction.java b/src/com/facebook/buck/rules/BuckPyFunction.java
new file mode 100644
index 0000000..c9f7a4e
--- /dev/null
+++ b/src/com/facebook/buck/rules/BuckPyFunction.java
@@ -0,0 +1,131 @@
+/*
+ * 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.rules;
+
+import com.facebook.buck.util.HumanReadableException;
+import com.google.common.base.Preconditions;
+import com.google.common.collect.Iterables;
+import com.google.common.collect.Sets;
+
+import java.util.SortedSet;
+
+/**
+ * Used to generate a function for use within buck.py for the rule described by a
+ * {@link Description}.
+ */
+class BuckPyFunction {
+
+ private final ArgObjectPopulatomatic argsInspector;
+
+ public BuckPyFunction(ArgObjectPopulatomatic argsInspector) {
+ this.argsInspector = Preconditions.checkNotNull(argsInspector);
+ }
+
+ String toPythonFunction(BuildRuleType type, Object dto) {
+ Preconditions.checkNotNull(type);
+ Preconditions.checkNotNull(dto);
+
+ StringBuilder builder = new StringBuilder();
+
+ SortedSet<ParamInfo> mandatory = Sets.newTreeSet();
+ SortedSet<ParamInfo> optional = Sets.newTreeSet();
+
+ for (ParamInfo param : argsInspector.getAllParamInfo(dto)) {
+ if (isSkippable(param)) {
+ continue;
+ }
+
+ if (param.isOptional()) {
+ optional.add(param);
+ } else {
+ mandatory.add(param);
+ }
+ }
+
+ builder.append("@provide_for_build\n")
+ .append("def ").append(type.getName()).append("(name, ");
+
+ // Construct the args.
+ for (ParamInfo param : Iterables.concat(mandatory, optional)) {
+ appendPythonParameter(builder, param);
+ }
+ builder.append("visibility=[], build_env=None):\n")
+
+ // Define the rule.
+ .append(" add_rule({\n")
+ .append(" 'type' : '").append(type.getName()).append("',\n")
+ .append(" 'name' : name,\n");
+
+ // Iterate over args.
+ for (ParamInfo param : Iterables.concat(mandatory, optional)) {
+ builder.append(" '")
+ .append(param.getName())
+ .append("' : ")
+ .append(param.getName())
+ .append(",\n");
+ }
+
+ builder.append(" 'visibility' : visibility,\n");
+ builder.append(" }, build_env)\n\n");
+
+ return builder.toString();
+ }
+
+ private void appendPythonParameter(StringBuilder builder, ParamInfo param) {
+ builder.append(param.getName());
+ if (param.isOptional()) {
+ builder.append("=").append(getPythonDefault(param));
+ }
+ builder.append(", ");
+ }
+
+ private String getPythonDefault(ParamInfo param) {
+ if (param.getContainerType() != null) {
+ return "[]";
+ }
+
+ if (Boolean.class.equals(param.getType())) {
+ return "False";
+ }
+
+ if (Number.class.isAssignableFrom(param.getType())) {
+ return "0";
+ }
+
+ if (String.class.equals(param.getType())) {
+ return "''";
+ }
+
+ return "None";
+ }
+
+ private boolean isSkippable(ParamInfo param) {
+ if ("name".equals(param.getName())) {
+ if (!String.class.equals(param.getType())) {
+ throw new HumanReadableException("'name' parameter must be a java.lang.String");
+ }
+ return true;
+ }
+
+ if ("visibility".equals(param.getName())) {
+ throw new HumanReadableException(
+ "'visibility' parameter must be omitted. It will be passed to the rule at run time.");
+ }
+
+ return false;
+ }
+}
diff --git a/src/com/facebook/buck/rules/Hint.java b/src/com/facebook/buck/rules/Hint.java
new file mode 100644
index 0000000..f5ae611
--- /dev/null
+++ b/src/com/facebook/buck/rules/Hint.java
@@ -0,0 +1,38 @@
+/*
+ * 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.rules;
+
+import static java.lang.annotation.ElementType.FIELD;
+import static java.lang.annotation.ElementType.METHOD;
+import static java.lang.annotation.RetentionPolicy.RUNTIME;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.Target;
+
+
+/**
+ * Represents hints given when deal with the value returned by
+ * {@link Description#getConstructorArg()}.
+ */
+@Retention(RUNTIME)
+@Target({FIELD, METHOD})
+public @interface Hint {
+ /**
+ * @return The name to use in preference to the field or property name (eg. "field_name")
+ */
+ String name();
+}
diff --git a/src/com/facebook/buck/rules/ParamInfo.java b/src/com/facebook/buck/rules/ParamInfo.java
new file mode 100644
index 0000000..0c75f73
--- /dev/null
+++ b/src/com/facebook/buck/rules/ParamInfo.java
@@ -0,0 +1,392 @@
+/*
+ * 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.rules;
+
+import static com.facebook.buck.model.BuildTarget.BUILD_TARGET_PREFIX;
+
+import com.facebook.buck.model.BuildTarget;
+import com.google.common.base.Optional;
+import com.google.common.base.Preconditions;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.ImmutableSortedSet;
+import com.google.common.collect.Lists;
+import com.google.common.primitives.Primitives;
+
+import java.lang.reflect.Field;
+import java.lang.reflect.ParameterizedType;
+import java.lang.reflect.Type;
+import java.lang.reflect.WildcardType;
+import java.nio.file.Path;
+import java.util.Collection;
+import java.util.List;
+import java.util.Set;
+import java.util.SortedSet;
+
+import javax.annotation.Nullable;
+
+/**
+ * Parameter information derived from the object returned by
+ * {@link Description#getConstructorArg()}.
+ */
+// DO NOT EXPOSE OUT OF PACKAGE.
+class ParamInfo implements Comparable<ParamInfo> {
+ private final String name;
+
+ /**
+ * Is the property optional (indicated by being Optional or @Hinted with a default value.
+ */
+ private final boolean isOptional;
+
+ /**
+ * If the value is a List or Set, which of the two is it.
+ */
+ @Nullable
+ private final Class<?> containerType;
+
+ /**
+ * The type of the field, or the contained type of an Optional or a Collection.
+ */
+ private final Class<?> type;
+ private final Path pathRelativeToProjectRoot;
+
+ private Field field;
+
+ /**
+ * @param pathRelativeToProjectRoot The path to the directory containing the build file this param
+ * is for.
+ * @param field The field in the constructor arg that this represents.
+ */
+ public ParamInfo(Path pathRelativeToProjectRoot, Field field) {
+ this.pathRelativeToProjectRoot = Preconditions.checkNotNull(pathRelativeToProjectRoot);
+
+ this.field = Preconditions.checkNotNull(field);
+ Hint hint = field.getAnnotation(Hint.class);
+ this.name = determineName(field.getName(), hint);
+
+ Class<?> rawType = field.getType();
+ Type genericType = field.getGenericType();
+
+ this.isOptional = Optional.class.isAssignableFrom(rawType);
+
+ // TODO(simons): Check for a wildcard here.
+ if (List.class.isAssignableFrom(rawType) || Set.class.isAssignableFrom(rawType)) {
+ this.containerType = rawType;
+ this.type = determineGenericType(genericType);
+ } else if (Optional.class.isAssignableFrom(rawType)) {
+ this.type = unwrapGenericType(genericType);
+
+ Class<?> container = null;
+
+ if (genericType instanceof ParameterizedType) {
+ Type containerType = ((ParameterizedType) genericType).getActualTypeArguments()[0];
+ if (containerType instanceof ParameterizedType) {
+ Type rawContainerType = ((ParameterizedType) containerType).getRawType();
+ if (!(rawContainerType instanceof Class)) {
+ throw new RuntimeException("Container type isn't a class: " + rawContainerType);
+ }
+ container = (Class<?>) rawContainerType;
+ if (!Collection.class.isAssignableFrom(container)) {
+ throw new RuntimeException("Cannot determine container type: " + container);
+ }
+ }
+ }
+ containerType = container;
+ } else {
+ this.containerType = null;
+ this.type = Primitives.wrap(rawType);
+ }
+ }
+
+ String getName() {
+ return name;
+ }
+
+ boolean isOptional() {
+ return isOptional;
+ }
+
+ @Nullable
+ Class<?> getContainerType() {
+ return containerType;
+ }
+
+ Class<?> getType() {
+ return type;
+ }
+
+ private Class<?> unwrapGenericType(Type type) {
+ if (!(type instanceof ParameterizedType)) {
+ return (Class<?>) type;
+ }
+
+ Type[] types = ((ParameterizedType) type).getActualTypeArguments();
+ if (types.length != 1) {
+ throw new IllegalArgumentException("Unable to determine generic type");
+ }
+
+ if (types[0] instanceof WildcardType) {
+ throw new IllegalStateException("Generic types must be specific: " + type);
+ }
+
+ if (types[0] instanceof ParameterizedType) {
+ return unwrapGenericType(types[0]);
+ }
+ return (Class<?>) types[0];
+ }
+
+ private String determineName(String name, @Nullable Hint hint) {
+ return hint == null ? name : hint.name();
+ }
+
+ private Class<?> determineGenericType(Type genericType) {
+ if (!(genericType instanceof ParameterizedType)) {
+ throw new IllegalArgumentException("Collection type was not generic");
+ }
+
+ Type[] types = ((ParameterizedType) genericType).getActualTypeArguments();
+ if (types.length != 1) {
+ throw new IllegalArgumentException("Unable to determine generic type");
+ }
+
+ if (types[0] instanceof WildcardType) {
+ WildcardType wild = (WildcardType) types[0];
+
+ if (Object.class.equals(wild.getUpperBounds()[0])) {
+ throw new IllegalArgumentException("Generic types must be specific: " + genericType);
+ }
+ return Primitives.wrap((Class<?>) wild.getUpperBounds()[0]);
+ }
+
+ return Primitives.wrap((Class<?>) types[0]);
+ }
+
+ public void setFromParams(
+ BuildRuleResolver ruleResolver,
+ Object dto,
+ BuildRuleFactoryParams params) {
+ if (containerType != null) {
+ set(ruleResolver, dto, params.getOptionalListAttribute(name));
+ } else if (Path.class.isAssignableFrom(type) ||
+ SourcePath.class.isAssignableFrom(type) ||
+ String.class.equals(type)) {
+ set(ruleResolver, dto, params.getOptionalStringAttribute(name).orNull());
+ } else if (Number.class.isAssignableFrom(type)) {
+ set(ruleResolver, dto, params.getRequiredLongAttribute(name));
+ } else if (BuildTarget.class.isAssignableFrom(type)) {
+ Optional<BuildTarget> optionalTarget = params.getOptionalBuildTarget(name);
+ set(ruleResolver, dto, optionalTarget.orNull());
+ } else if (BuildRule.class.isAssignableFrom(type)) {
+ Optional<BuildTarget> optionalTarget = params.getOptionalBuildTarget(name);
+ set(ruleResolver, dto, optionalTarget.orNull());
+ } else if (Boolean.class.equals(type)) {
+ set(ruleResolver, dto, params.getBooleanAttribute(name));
+ } else {
+ throw new RuntimeException("Unknown type: " + type);
+ }
+ }
+
+ /**
+ * Sets a single property of the {@code dto}, coercing types as necessary.
+ *
+ * @param resolver {@link com.facebook.buck.rules.BuildRuleResolver} used for {@link com.facebook.buck.rules.BuildRule} instances.
+ * @param dto The constructor DTO on which the value should be set.
+ * @param value The value, which may be coerced depending on the type on {@code dto}.
+ */
+ public void set(BuildRuleResolver resolver, Object dto, @Nullable Object value) {
+ if (value == null) {
+ if (!isOptional) {
+ throw new IllegalArgumentException(String.format(
+ "%s cannot be null. Build file can be found in %s.",
+ dto, pathRelativeToProjectRoot));
+ }
+
+ value = Optional.absent();
+ } else if (isOptional && matchesDefaultValue(value)) {
+ if (value instanceof Collection) {
+ value = asCollection(resolver, Lists.newArrayList());
+ value = Optional.of(value);
+ } else {
+ value = Optional.absent();
+ }
+ } else if (containerType == null) {
+ value = coerceToExpectedType(resolver, value);
+ if (isOptional) {
+ value = Optional.of(value);
+ }
+ } else {
+ value = asCollection(resolver, value);
+ if (isOptional) {
+ value = Optional.of(value);
+ }
+ }
+
+ try {
+ field.set(dto, value);
+ } catch (ReflectiveOperationException e) {
+ throw new RuntimeException(e);
+ } catch (ClassCastException | IllegalArgumentException | NullPointerException e) {
+ throw new IllegalArgumentException(String.format(
+ "Unable to convert '%s' to %s in build file in %s",
+ value, type, pathRelativeToProjectRoot));
+ }
+ }
+
+ private boolean matchesDefaultValue(Object value) {
+ if (value instanceof String && "".equals(value)) {
+ return true;
+ }
+
+ if (value instanceof Number && ((Number)value).intValue() == 0) {
+ return true;
+ }
+
+ if (Boolean.FALSE.equals(value)) {
+ return true;
+ }
+
+ if (value instanceof List && ((List) value).isEmpty()) {
+ return true;
+ }
+
+ return false;
+ }
+
+ private Object coerceToExpectedType(BuildRuleResolver ruleResolver, Object value) {
+ if (BuildRule.class.isAssignableFrom(type)) {
+ BuildTarget target = asBuildTarget(value);
+ return ruleResolver.get(target);
+ }
+
+ if (BuildTarget.class.isAssignableFrom(type)) {
+ return asBuildTarget(value);
+ }
+
+ // All paths should be relative to the base path.
+ if (Path.class.isAssignableFrom(type)) {
+ return asNormalizedPath(value);
+ }
+
+ if (SourcePath.class.isAssignableFrom(type)) {
+ BuildTarget target = asBuildTarget(value);
+ if (target != null) {
+ return new BuildTargetSourcePath(target);
+ }
+ Path path = asNormalizedPath(value);
+ return new FileSourcePath(pathRelativeToProjectRoot.relativize(path).toString());
+ }
+
+ if (value instanceof Number) {
+ Number num = (Number) value;
+ if (Double.class.equals(type)) {
+ return num.doubleValue();
+ } else if (Integer.class.equals(type)) {
+ return num.intValue();
+ } else if (Float.class.equals(type)) {
+ return num.floatValue();
+ } else if (Long.class.equals(type)) {
+ return num.longValue(); // not strictly necessary, but included for completeness.
+ } else if (Short.class.equals(type)) {
+ return num.shortValue();
+ }
+ }
+
+ // We're going to cheat and let the JVM take the strain of converting between primitive and
+ // object wrapper types, but it has a habit of coercing to a String that should be avoided.
+ if (String.class.equals(type) && !String.class.equals(value.getClass())) {
+ throw new IllegalArgumentException(
+ String.format("Unable to convert '%s' to %s", value, type));
+ }
+
+ return value;
+ }
+
+ @Nullable
+ private BuildTarget asBuildTarget(Object value) {
+ if (value instanceof BuildTarget) {
+ return (BuildTarget) value;
+ }
+
+ Preconditions.checkArgument(value instanceof String,
+ "Expected argument '%s' to be a build target", value);
+
+ String param = (String) value;
+ int colon = param.indexOf(':');
+ if (colon == 0 && param.length() > 1) {
+ return new BuildTarget(
+ BUILD_TARGET_PREFIX + pathRelativeToProjectRoot.toString(),
+ param.substring(1));
+ } else if (colon > 0 && param.length() > 2) {
+ return new BuildTarget(param.substring(0, colon), param.substring(colon + 1));
+ }
+ return null;
+ }
+
+ private Path asNormalizedPath(Object value) {
+ Preconditions.checkArgument(value instanceof String,
+ "Expected argument '%s' to be a string in build file in %s",
+ value, pathRelativeToProjectRoot);
+
+ return pathRelativeToProjectRoot.resolve((String) value).normalize();
+ }
+
+ @SuppressWarnings("unchecked")
+ private Object asCollection(BuildRuleResolver ruleResolver, Object value) {
+ if (!(value instanceof Collection)) {
+ throw new IllegalArgumentException(String.format(
+ "May not set '%s' on a collection type in build file in %s",
+ value, pathRelativeToProjectRoot));
+ }
+
+ List<Object> collection = Lists.newArrayList();
+
+ for (Object obj : (Iterable<Object>) value) {
+ collection.add(coerceToExpectedType(ruleResolver, obj));
+ }
+
+ if (SortedSet.class.isAssignableFrom(containerType)) {
+ return ImmutableSortedSet.copyOf(collection);
+ } else if (Set.class.isAssignableFrom(containerType)) {
+ return ImmutableSet.copyOf(collection);
+ } else {
+ return ImmutableList.copyOf(collection);
+ }
+ }
+
+ /**
+ * Only valid when comparing {@link ParamInfo} instances from the same description.
+ */
+ @Override
+ public int compareTo(ParamInfo that) {
+ return this.name.compareTo(that.name);
+ }
+
+ @Override
+ public int hashCode() {
+ return name.hashCode();
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (!(obj instanceof ParamInfo)) {
+ return false;
+ }
+
+ ParamInfo that = (ParamInfo) obj;
+ return name.equals(that.getName());
+ }
+}
diff --git a/test/com/facebook/buck/rules/ArgObjectPopulatomaticTest.java b/test/com/facebook/buck/rules/ArgObjectPopulatomaticTest.java
new file mode 100644
index 0000000..a207fd6
--- /dev/null
+++ b/test/com/facebook/buck/rules/ArgObjectPopulatomaticTest.java
@@ -0,0 +1,480 @@
+/*
+ * 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.rules;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import com.facebook.buck.model.BuildTarget;
+import com.facebook.buck.model.BuildTargetFactory;
+import com.facebook.buck.parser.BuildTargetParser;
+import com.facebook.buck.util.ProjectFilesystem;
+import com.google.common.base.Optional;
+import com.google.common.base.Throwables;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.ImmutableSortedSet;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
+import com.google.common.collect.Sets;
+
+import org.junit.Before;
+import org.junit.Test;
+
+import java.io.File;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+@SuppressWarnings("unused") // Many unused fields in sample DTO objects.
+public class ArgObjectPopulatomaticTest {
+
+ private Path basePath;
+ private ArgObjectPopulatomatic inspector;
+ private BuildRuleResolver ruleResolver;
+
+ @Before
+ public void setUpInspector() {
+ basePath = Paths.get("example", "path");
+ inspector = new ArgObjectPopulatomatic(basePath);
+ ruleResolver = new BuildRuleResolver();
+ }
+
+ @Test
+ public void shouldNotPopulateAnEmptyArg() {
+ class Dto {
+ }
+
+ Dto dto = new Dto();
+ try {
+ inspector.populate(
+ ruleResolver, buildRuleFactoryParams(ImmutableMap.<String, Object>of()), dto);
+ } catch (RuntimeException e) {
+ fail("Did not expect an exception to be thrown:\n" + Throwables.getStackTraceAsString(e));
+ }
+ }
+
+ @Test
+ public void shouldPopulateAStringValue() {
+ class Dto {
+ public String name;
+ }
+
+ Dto dto = new Dto();
+ inspector.populate(
+ ruleResolver,
+ buildRuleFactoryParams(ImmutableMap.<String, Object>of("name", "cheese")),
+ dto);
+
+ assertEquals("cheese", dto.name);
+ }
+
+ @Test
+ public void shouldPopulateABooleanValue() {
+ class Dto {
+ public boolean value;
+ }
+
+ Dto dto = new Dto();
+ inspector.populate(ruleResolver,
+ buildRuleFactoryParams(ImmutableMap.<String, Object>of("value", true)),
+ dto);
+
+ assertTrue(dto.value);
+ }
+
+ @Test
+ public void shouldPopulateBuildTargetValues() {
+ class Dto {
+ public BuildTarget target;
+ public BuildTarget local;
+ }
+
+ Dto dto = new Dto();
+ inspector.populate(ruleResolver,
+ buildRuleFactoryParams(ImmutableMap.<String, Object>of(
+ "target", "//cake:walk",
+ "local", ":fish"
+ )),
+ dto);
+
+ assertEquals(BuildTargetFactory.newInstance("//cake:walk"), dto.target);
+ assertEquals(BuildTargetFactory.newInstance("//example/path:fish"), dto.local);
+ }
+
+ @Test
+ public void shouldPopulateANumericValue() {
+ class Dto {
+ public long number;
+ }
+
+ Dto dto = new Dto();
+ inspector.populate(ruleResolver,
+ buildRuleFactoryParams(ImmutableMap.<String, Object>of("number", 42L)),
+ dto);
+
+ assertEquals(42, dto.number);
+ }
+
+ @Test
+ public void shouldPopulateAPathValue() {
+ class Dto {
+ @Hint(name = "some_path")
+ public Path somePath;
+ }
+
+ Dto dto = new Dto();
+ inspector.populate(ruleResolver,
+ buildRuleFactoryParams(ImmutableMap.<String, Object>of("some_path", "Fish.java")),
+ dto);
+
+ assertEquals(Paths.get("example/path", "Fish.java"), dto.somePath);
+ }
+
+ @Test
+ public void shouldPopulateSourcePaths() {
+ class Dto {
+ public SourcePath filePath;
+ public SourcePath targetPath;
+ }
+
+ BuildTarget target = BuildTargetFactory.newInstance("//example/path:peas");
+ Dto dto = new Dto();
+ inspector.populate(
+ ruleResolver,
+ buildRuleFactoryParams(ImmutableMap.<String, Object>of(
+ "filePath", "cheese.txt",
+ "targetPath", ":peas"
+ )),
+ dto);
+
+ assertEquals(new FileSourcePath("cheese.txt"), dto.filePath);
+ assertEquals(new BuildTargetSourcePath(target), dto.targetPath);
+ }
+
+ @Test
+ public void shouldPopulateAnImmutableSortedSet() {
+ class Dto {
+ public ImmutableSortedSet<BuildTarget> deps;
+ }
+
+ BuildTarget t1 = BuildTargetFactory.newInstance("//please/go:here");
+ BuildTarget t2 = BuildTargetFactory.newInstance("//example/path:there");
+
+ Dto dto = new Dto();
+ // Note: the ordering is reversed from the natural ordering
+ inspector.populate(
+ ruleResolver,
+ buildRuleFactoryParams(ImmutableMap.<String, Object>of(
+ "deps", ImmutableList.of("//please/go:here", ":there"))),
+ dto);
+
+ assertEquals(ImmutableSortedSet.of(t2, t1), dto.deps);
+ }
+
+ @Test
+ public void shouldPopulateSets() {
+ class Dto {
+ public Set<Path> paths;
+ }
+
+ Dto dto = new Dto();
+ inspector.populate(
+ ruleResolver,
+ buildRuleFactoryParams(ImmutableMap.<String, Object>of(
+ "paths", ImmutableList.of("one", "two"))),
+ dto);
+
+ assertEquals(
+ ImmutableSet.of(Paths.get("example/path/one"), Paths.get("example/path/two")),
+ dto.paths);
+ }
+
+ @Test
+ public void shouldPopulateLists() {
+ class Dto {
+ public List<String> list;
+ }
+
+ Dto dto = new Dto();
+ inspector.populate(
+ ruleResolver,
+ buildRuleFactoryParams(ImmutableMap.<String, Object>of(
+ "list", ImmutableList.of("alpha", "beta"))),
+ dto);
+
+ assertEquals(ImmutableList.of("alpha", "beta"), dto.list);
+ }
+
+ @Test
+ public void collectionsCanBeOptionalAndWillBeSetToAnOptionalEmptyCollectionIfMissing() {
+ class Dto {
+ public Optional<Set<BuildTarget>> targets;
+ }
+
+ Dto dto = new Dto();
+ Map<String, Object> args = Maps.newHashMap();
+ args.put("targets", Lists.newArrayList());
+
+ inspector.populate(ruleResolver, buildRuleFactoryParams(args), dto);
+
+ assertEquals(Optional.of(Sets.newHashSet()), dto.targets);
+ }
+
+ @Test(expected = RuntimeException.class)
+ public void shouldBeAnErrorToAttemptToSetASingleValueToACollection() {
+ class Dto {
+ public String file;
+ }
+
+ Dto dto = new Dto();
+ inspector.populate(
+ ruleResolver,
+ buildRuleFactoryParams(ImmutableMap.<String, Object>of("file", ImmutableList.of("a", "b"))),
+ dto);
+ }
+
+ @Test(expected = RuntimeException.class)
+ public void shouldBeAnErrorToAttemptToSetACollectionToASingleValue() {
+ class Dto {
+ public Set<String> strings;
+ }
+
+ Dto dto = new Dto();
+ inspector.populate(
+ ruleResolver,
+ buildRuleFactoryParams(ImmutableMap.<String, Object>of("strings", "isn't going to happen")),
+ dto);
+ }
+
+ @Test(expected = RuntimeException.class)
+ public void shouldBeAnErrorToSetTheWrongTypeOfValueInACollection() {
+ class Dto {
+ public Set<String> strings;
+ }
+
+ Dto dto = new Dto();
+ inspector.populate(
+ ruleResolver,
+ buildRuleFactoryParams(ImmutableMap.<String, Object>of(
+ "strings", ImmutableSet.of(true, false))),
+ dto);
+ }
+
+ @Test
+ public void shouldNormalizePaths() {
+ class Dto {
+ public Path path;
+ }
+
+ Dto dto = new Dto();
+ inspector.populate(
+ ruleResolver,
+ buildRuleFactoryParams(ImmutableMap.<String, Object>of("path", "./foo/../bar/./fish.txt")),
+ dto);
+
+ assertEquals(basePath.resolve("bar/fish.txt").normalize(), dto.path);
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void lowerBoundGenericTypesCauseAnException() {
+ class Dto {
+ public List<? super BuildTarget> nope;
+ }
+
+ inspector.populate(
+ ruleResolver,
+ buildRuleFactoryParams(ImmutableMap.<String, Object>of(
+ "nope", ImmutableList.of("//will/not:happen"))),
+ new Dto());
+ }
+
+ public void shouldSetBuildTargetParameters() {
+ class Dto {
+ public BuildTarget single;
+ public BuildTarget sameBuildFileTarget;
+ public List<BuildTarget> targets;
+ }
+ Dto dto = new Dto();
+
+ inspector.populate(
+ ruleResolver,
+ buildRuleFactoryParams(ImmutableMap.<String, Object>of(
+ "single", "//com/example:cheese",
+ "sameBuildFileTarget", ":cake",
+ "targets", ImmutableList.of(":cake", "//com/example:cheese")
+ )),
+ dto);
+
+ BuildTarget cheese = BuildTargetFactory.newInstance("//com/example:cheese");
+ BuildTarget cake = BuildTargetFactory.newInstance("//example/path:cake");
+
+ assertEquals(cheese, dto.single);
+ assertEquals(cake, dto.sameBuildFileTarget);
+ assertEquals(ImmutableList.of(cake, cheese), dto.targets);
+ }
+
+ @Test
+ public void shouldSetBuildRulesIfRequested() {
+ class Dto {
+ public BuildRule directDep;
+ }
+
+ BuildTarget target = BuildTargetFactory.newInstance("//some/exmaple:target");
+ BuildRule rule = new FakeBuildRule(new BuildRuleType("example"), target);
+
+ BuildRuleResolver resolver = new BuildRuleResolver(ImmutableMap.of(target, rule));
+
+ Dto dto = new Dto();
+ inspector.populate(
+ resolver,
+ buildRuleFactoryParams(ImmutableMap.<String, Object>of(
+ "directDep", target.getFullyQualifiedName())),
+ dto);
+
+ assertEquals(dto.directDep, rule);
+ }
+
+ @Test
+ public void upperBoundGenericTypesCauseValuesToBeSetToTheUpperBound() {
+ class Dto {
+ public List<? extends SourcePath> yup;
+ }
+
+ BuildTarget target = BuildTargetFactory.newInstance("//will:happen");
+ Dto dto = new Dto();
+ inspector.populate(
+ ruleResolver,
+ buildRuleFactoryParams(ImmutableMap.<String, Object>of(
+ "yup", ImmutableList.of(target.getFullyQualifiedName()))),
+ dto);
+
+ BuildTargetSourcePath path = new BuildTargetSourcePath(target);
+ assertEquals(ImmutableList.of(path), dto.yup);
+ }
+
+ @Test
+ public void canPopulateSimpleConstructorArgFromBuildFactoryParams() {
+ class Dto {
+ public String required;
+ public Optional<String> notRequired;
+
+ public int num;
+ // Turns out there no optional number params
+ //public Optional<Long> optionalLong;
+
+ public boolean needed;
+ // Turns out there are no optional boolean params
+ //public Optional<Boolean> notNeeded;
+
+ public SourcePath aSrcPath;
+ public Optional<SourcePath> notASrcPath;
+
+ public Path aPath;
+ public Optional<Path> notAPath;
+ }
+
+ ImmutableMap<String, Object> args = ImmutableMap.<String, Object>builder()
+ .put("required", "cheese")
+ .put("notRequired", "cake")
+ // Long because that's what comes from python.
+ .put("num", 42L)
+ // Skipping optional Long.
+ .put("needed", true)
+ // Skipping optional boolean.
+ .put("aSrcPath", ":path")
+ .put("aPath", "./File.java")
+ .put("notAPath", "./NotFile.java")
+ .build();
+ Dto dto = new Dto();
+ inspector.populate(ruleResolver, buildRuleFactoryParams(args), dto);
+
+ assertEquals("cheese", dto.required);
+ assertEquals("cake", dto.notRequired.get());
+ assertEquals(42, dto.num);
+ assertTrue(dto.needed);
+ BuildTargetSourcePath expected = new BuildTargetSourcePath(
+ BuildTargetFactory.newInstance("//example/path:path"));
+ assertEquals(expected, dto.aSrcPath);
+ assertEquals(Paths.get("example/path/NotFile.java"), dto.notAPath.get());
+ }
+
+ @Test
+ /**
+ * Since we populated the params from the python script, and the python script inserts default
+ * values instead of nulls it's never possible for someone to see "Optional.absent()", but that's
+ * what we want as authors of buildables. Handle that case.
+ */
+ public void shouldPopulateDefaultValuesAsBeingAbsent() {
+ class Dto {
+ public Optional<String> noString;
+ public Optional<String> defaultString;
+
+ public Optional<SourcePath> noSourcePath;
+ public Optional<SourcePath> defaultSourcePath;
+ }
+
+ ImmutableMap<String, Object> args = ImmutableMap.<String, Object>builder()
+ .put("defaultString", "")
+ .put("defaultSourcePath", "")
+ .build();
+ Dto dto = new Dto();
+ inspector.populate(ruleResolver, buildRuleFactoryParams(args), dto);
+
+ assertEquals(Optional.absent(), dto.noString);
+ assertEquals(Optional.absent(), dto.defaultString);
+ assertEquals(Optional.absent(), dto.noSourcePath);
+ assertEquals(Optional.absent(), dto.defaultSourcePath);
+ }
+
+ @Test
+ public void shouldResolveBuildRulesFromTargetsAndAssignToFields() {
+ class Dto {
+ public BuildRule rule;
+ }
+
+ BuildTarget target = BuildTargetFactory.newInstance("//i/love:lucy");
+ BuildRule rule = new FakeBuildRule(new BuildRuleType("example"), target);
+ BuildRuleResolver resolver = new BuildRuleResolver(ImmutableMap.of(target, rule));
+
+ Dto dto = new Dto();
+ inspector.populate(
+ resolver,
+ buildRuleFactoryParams(
+ ImmutableMap.<String, Object>of("rule", target.getFullyQualifiedName())),
+ dto);
+
+ assertEquals(rule, dto.rule);
+ }
+
+ public BuildRuleFactoryParams buildRuleFactoryParams(Map<String, Object> args) {
+ ProjectFilesystem filesystem = new ProjectFilesystem(new File(".")) {
+ @Override
+ public boolean exists(String pathRelativeToProjectRoot) {
+ return true;
+ }
+ };
+ BuildTargetParser parser = new BuildTargetParser(filesystem);
+ BuildTarget target = BuildTargetFactory.newInstance("//example/path:three");
+ return NonCheckingBuildRuleFactoryParams.createNonCheckingBuildRuleFactoryParams(
+ args, parser, target);
+ }
+}
diff --git a/test/com/facebook/buck/rules/BuckPyFunctionTest.java b/test/com/facebook/buck/rules/BuckPyFunctionTest.java
new file mode 100644
index 0000000..c094549
--- /dev/null
+++ b/test/com/facebook/buck/rules/BuckPyFunctionTest.java
@@ -0,0 +1,142 @@
+/*
+ * 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.rules;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import com.facebook.buck.model.BuildTarget;
+import com.facebook.buck.model.BuildTargetPattern;
+import com.facebook.buck.util.HumanReadableException;
+import com.google.common.base.Joiner;
+import com.google.common.base.Optional;
+
+import org.junit.Before;
+import org.junit.Test;
+
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.List;
+import java.util.Set;
+
+@SuppressWarnings("unused") // Many unused fields in sample DTO objects.
+public class BuckPyFunctionTest {
+
+ private BuckPyFunction buckPyFunction;
+
+ @Before
+ public void setUpInspector() {
+ Path basePath = Paths.get("example", "path");
+ buckPyFunction = new BuckPyFunction(new ArgObjectPopulatomatic(basePath));
+ }
+
+ @Test
+ public void nameWillBeAddedIfMissing() {
+ class NoName { public String random; }
+
+ String definition = buckPyFunction.toPythonFunction(new BuildRuleType("bad"), new NoName());
+
+ assertTrue(definition.contains("name"));
+ }
+
+ @Test
+ public void visibilityWillBeAddedIfMissing() {
+ class NoVis { public String random; }
+
+ String definition = buckPyFunction.toPythonFunction(new BuildRuleType("bad"), new NoVis());
+
+ assertTrue(definition.contains("visibility=[]"));
+ }
+
+ @Test
+ public void shouldOnlyIncludeTheNameFieldOnce() {
+ class Named { public String name; }
+
+ String definition = buckPyFunction.toPythonFunction(new BuildRuleType("named"), new Named());
+
+ assertEquals(Joiner.on("\n").join(
+ "@provide_for_build",
+ "def named(name, visibility=[], build_env=None):",
+ " add_rule({",
+ " 'type' : 'named',",
+ " 'name' : name,",
+ " 'visibility' : visibility,",
+ " }, build_env)",
+ "",
+ ""
+ ), definition);
+ }
+
+ @Test(expected = HumanReadableException.class)
+ public void theNameFieldMustBeAString() {
+ class BadName { public int name; }
+
+ buckPyFunction.toPythonFunction(new BuildRuleType("nope"), new BadName());
+ }
+
+ @Test
+ public void optionalFieldsAreGivenSensibleDefaultValues() {
+ class LotsOfOptions {
+ public Optional<String> thing;
+ public Optional<List<BuildTarget>> targets;
+ public Optional<Integer> version;
+ }
+
+ String definition = buckPyFunction.toPythonFunction(
+ new BuildRuleType("optional"), new LotsOfOptions());
+
+ assertTrue(definition, definition.contains("targets=[], thing='', version=0"));
+ }
+
+ @Test
+ public void optionalFieldsAreListedAfterMandatoryOnes() {
+ class Either {
+ // Alphabetical ordering is deliberate.
+ public Optional<String> cat;
+ public String dog;
+ public Optional<String> egg;
+ public String fake;
+ }
+
+ String definition = buckPyFunction.toPythonFunction(new BuildRuleType("either"), new Either());
+
+ assertEquals(Joiner.on("\n").join(
+ "@provide_for_build",
+ "def either(name, dog, fake, cat='', egg='', visibility=[], build_env=None):",
+ " add_rule({",
+ " 'type' : 'either',",
+ " 'name' : name,",
+ " 'dog' : dog,",
+ " 'fake' : fake,",
+ " 'cat' : cat,",
+ " 'egg' : egg,",
+ " 'visibility' : visibility,",
+ " }, build_env)",
+ "",
+ ""
+ ), definition);
+ }
+
+ @Test(expected = HumanReadableException.class)
+ public void visibilityOptionsMustNotBeSetAsTheyArePassedInBuildRuleParamsLater() {
+ class Visible {
+ public Set<BuildTargetPattern> visibility;
+ }
+
+ buckPyFunction.toPythonFunction(new BuildRuleType("nope"), new Visible());
+ }
+}