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&gt;SourcePath> srcs;
+ *       @Hint(name = "optional_thing")
+ *       public Optional&gt;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());
+  }
+}