Move expansion functionality to a new class

Progress on #2475.

PiperOrigin-RevId: 170473111
diff --git a/src/main/java/com/google/devtools/build/lib/analysis/Expander.java b/src/main/java/com/google/devtools/build/lib/analysis/Expander.java
new file mode 100644
index 0000000..69f45f9
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/analysis/Expander.java
@@ -0,0 +1,215 @@
+// Copyright 2017 The Bazel Authors. All rights reserved.
+//
+// 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.google.devtools.build.lib.analysis;
+
+import com.google.common.collect.ImmutableList;
+import com.google.devtools.build.lib.analysis.LocationExpander.Options;
+import com.google.devtools.build.lib.shell.ShellUtils;
+import com.google.devtools.build.lib.syntax.Type;
+import java.util.ArrayList;
+import java.util.List;
+import javax.annotation.Nullable;
+
+/**
+ * Expansion of strings and string lists by replacing make variables and $(location) functions.
+ */
+public final class Expander {
+  /** Indicates whether a string list attribute should be tokenized. */
+  private enum Tokenize {
+    YES,
+    NO
+  }
+
+  private final RuleContext ruleContext;
+  private final ConfigurationMakeVariableContext makeVariableContext;
+  @Nullable private final LocationExpander locationExpander;
+
+  private Expander(
+      RuleContext ruleContext,
+      ConfigurationMakeVariableContext makeVariableContext,
+      @Nullable LocationExpander locationExpander) {
+    this.ruleContext = ruleContext;
+    this.makeVariableContext = makeVariableContext;
+    this.locationExpander = locationExpander;
+  }
+
+  Expander(
+      RuleContext ruleContext,
+      ConfigurationMakeVariableContext makeVariableContext) {
+    this(ruleContext, makeVariableContext, null);
+  }
+
+  /**
+   * Returns a new instance that also expands locations using the default configuration of
+   * {@link LocationExpander}.
+   */
+  public Expander withLocations(Options... options) {
+    LocationExpander newLocationExpander =
+        new LocationExpander(ruleContext, options);
+    return new Expander(ruleContext, makeVariableContext, newLocationExpander);
+  }
+
+  /**
+   * Returns a new instance that also expands locations, passing {@link Options#ALLOW_DATA} to the
+   * underlying {@link LocationExpander}.
+   */
+  public Expander withDataLocations() {
+    return withLocations(Options.ALLOW_DATA);
+  }
+
+  /**
+   * Returns a new instance that also expands locations, passing {@link Options#ALLOW_DATA} and
+   * {@link Options#EXEC_PATHS} to the underlying {@link LocationExpander}.
+   */
+  public Expander withDataExecLocations() {
+    return withLocations(Options.ALLOW_DATA, Options.EXEC_PATHS);
+  }
+
+  /**
+   * Expands the given value string, tokenizes it, and then adds it to the given list. The attribute
+   * name is only used for error reporting.
+   */
+  public void tokenizeAndExpandMakeVars(
+      List<String> result,
+      String attributeName,
+      String value) {
+    expandValue(result, attributeName, value, Tokenize.YES);
+  }
+
+  /**
+   * Expands make variables and $(location) tags in value, and optionally tokenizes the result.
+   */
+  private void expandValue(
+      List<String> tokens,
+      String attributeName,
+      String value,
+      Tokenize tokenize) {
+    value = expand(attributeName, value);
+    if (tokenize == Tokenize.YES) {
+      try {
+        ShellUtils.tokenize(tokens, value);
+      } catch (ShellUtils.TokenizationException e) {
+        ruleContext.attributeError(attributeName, e.getMessage());
+      }
+    } else {
+      tokens.add(value);
+    }
+  }
+
+  /**
+   * Returns the string "expression" after expanding all embedded references to
+   * "Make" variables.  If any errors are encountered, they are reported, and
+   * "expression" is returned unchanged.
+   *
+   * @param attributeName the name of the attribute
+   * @return the expansion of "expression".
+   */
+  public String expand(String attributeName) {
+    return expand(attributeName, ruleContext.attributes().get(attributeName, Type.STRING));
+  }
+
+  /**
+   * Returns the string "expression" after expanding all embedded references to
+   * "Make" variables.  If any errors are encountered, they are reported, and
+   * "expression" is returned unchanged.
+   *
+   * @param attributeName the name of the attribute from which "expression" comes;
+   *     used for error reporting.
+   * @param expression the string to expand.
+   * @return the expansion of "expression".
+   */
+  public String expand(String attributeName, String expression) {
+    if (locationExpander != null) {
+      expression = locationExpander.expandAttribute(attributeName, expression);
+    }
+    try {
+      return MakeVariableExpander.expand(expression, makeVariableContext);
+    } catch (MakeVariableExpander.ExpansionException e) {
+      ruleContext.attributeError(attributeName, e.getMessage());
+      return expression;
+    }
+  }
+
+  /**
+   * Expands all the strings in the given list, optionally tokenizing them after expansion. The
+   * attribute name is only used for error reporting.
+   */
+  private ImmutableList<String> expandAndTokenizeList(
+      String attrName, List<String> values, Tokenize tokenize) {
+    List<String> variables = new ArrayList<>();
+    for (String variable : values) {
+      expandValue(variables, attrName, variable, tokenize);
+    }
+    return ImmutableList.copyOf(variables);
+  }
+
+  /**
+   * Obtains the value of the attribute, expands all values, and returns the resulting list. If the
+   * attribute does not exist or is not of type {@link Type#STRING_LIST}, then this method returns
+   * an empty list.
+   */
+  public ImmutableList<String> list(String attrName) {
+    if (!ruleContext.getRule().isAttrDefined(attrName, Type.STRING_LIST)) {
+      // TODO(bazel-team): This should be an error.
+      return ImmutableList.of();
+    }
+    return list(attrName, ruleContext.attributes().get(attrName, Type.STRING_LIST));
+  }
+
+  /**
+   * Expands all the strings in the given list. The attribute name is only used for error reporting.
+   */
+  public ImmutableList<String> list(String attrName, List<String> values) {
+    return expandAndTokenizeList(attrName, values, Tokenize.NO);
+  }
+
+  /**
+   * Obtains the value of the attribute, expands, and tokenizes all values. If the attribute does
+   * not exist or is not of type {@link Type#STRING_LIST}, then this method returns an empty list.
+   */
+  public ImmutableList<String> tokenized(String attrName) {
+    if (!ruleContext.getRule().isAttrDefined(attrName, Type.STRING_LIST)) {
+      // TODO(bazel-team): This should be an error.
+      return ImmutableList.of();
+    }
+    return tokenized(attrName, ruleContext.attributes().get(attrName, Type.STRING_LIST));
+  }
+
+  /**
+   * Expands all the strings in the given list, and tokenizes them after expansion. The attribute
+   * name is only used for error reporting.
+   */
+  public ImmutableList<String> tokenized(String attrName, List<String> values) {
+    return expandAndTokenizeList(attrName, values, Tokenize.YES);
+  }
+
+  /**
+   * If the string consists of a single variable, returns the expansion of that variable. Otherwise,
+   * returns null. Syntax errors are reported.
+   *
+   * @param attrName the name of the attribute from which "expression" comes; used for error
+   *     reporting.
+   * @param expression the string to expand.
+   * @return the expansion of "expression", or null.
+   */
+  @Nullable
+  public String expandSingleMakeVariable(String attrName, String expression) {
+    try {
+      return MakeVariableExpander.expandSingleVariable(expression, makeVariableContext);
+    } catch (MakeVariableExpander.ExpansionException e) {
+      ruleContext.attributeError(attrName, e.getMessage());
+      return expression;
+    }
+  }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/analysis/LocationExpander.java b/src/main/java/com/google/devtools/build/lib/analysis/LocationExpander.java
index 5d68516..9d3fa1d 100644
--- a/src/main/java/com/google/devtools/build/lib/analysis/LocationExpander.java
+++ b/src/main/java/com/google/devtools/build/lib/analysis/LocationExpander.java
@@ -131,7 +131,7 @@
    * Expands attribute's location and locations tags based on the target and
    * location map.
    *
-   * @param attrName  name of the attribute
+   * @param attrName  name of the attribute; only used for error reporting
    * @param attrValue initial value of the attribute
    * @return attribute value with expanded location tags or original value in
    *         case of errors
diff --git a/src/main/java/com/google/devtools/build/lib/analysis/RuleContext.java b/src/main/java/com/google/devtools/build/lib/analysis/RuleContext.java
index 9a114f5..e17f74a 100644
--- a/src/main/java/com/google/devtools/build/lib/analysis/RuleContext.java
+++ b/src/main/java/com/google/devtools/build/lib/analysis/RuleContext.java
@@ -38,7 +38,6 @@
 import com.google.devtools.build.lib.actions.ArtifactOwner;
 import com.google.devtools.build.lib.actions.Root;
 import com.google.devtools.build.lib.analysis.ConfiguredRuleClassProvider.PrerequisiteValidator;
-import com.google.devtools.build.lib.analysis.LocationExpander.Options;
 import com.google.devtools.build.lib.analysis.actions.ActionConstructionContext;
 import com.google.devtools.build.lib.analysis.buildinfo.BuildInfoFactory.BuildInfoKey;
 import com.google.devtools.build.lib.analysis.config.BuildConfiguration;
@@ -80,7 +79,6 @@
 import com.google.devtools.build.lib.packages.RuleErrorConsumer;
 import com.google.devtools.build.lib.packages.Target;
 import com.google.devtools.build.lib.packages.TargetUtils;
-import com.google.devtools.build.lib.shell.ShellUtils;
 import com.google.devtools.build.lib.syntax.EvalException;
 import com.google.devtools.build.lib.syntax.Type;
 import com.google.devtools.build.lib.syntax.Type.LabelClass;
@@ -972,96 +970,12 @@
     initConfigurationMakeVariableContext(ImmutableList.copyOf(makeVariableSuppliers));
   }
 
-  /** Indicates whether a string list attribute should be tokenized. */
-  public enum Tokenize {
-    YES,
-    NO
+  public Expander getExpander(ConfigurationMakeVariableContext makeVariableContext) {
+    return new Expander(this, makeVariableContext);
   }
 
-  /**
-   * Gets an attribute of type STRING_LIST expanding Make variables, $(location) tags into the
-   * dependency location (see {@link LocationExpander} for details) and tokenizes the result.
-   *
-   * @param attributeName the name of the attribute to process
-   * @return a list of strings containing the expanded and tokenized values for the attribute
-   */
-  public ImmutableList<String> getTokenizedStringListAttr(String attributeName) {
-    return getExpandedStringListAttr(attributeName, Tokenize.YES);
-  }
-
-  /**
-   * Gets an attribute of type STRING_LIST expanding Make variables and $(location) tags, and
-   * optionally tokenizes the result. Doesn't register any {@link MakeVariableSupplier}.
-   *
-   * @param attributeName the name of the attribute to process
-   * @return a list of strings containing the processed values for the attribute
-   */
-  public ImmutableList<String> getExpandedStringListAttr(String attributeName) {
-    return getExpandedStringListAttr(attributeName, Tokenize.NO);
-  }
-
-  /**
-   * Gets an attribute of type STRING_LIST expanding Make variables and $(location) tags, and
-   * optionally tokenizes the result.
-   *
-   * @param attributeName the name of the attribute to process
-   * @return a list of strings containing the processed values for the attribute
-   */
-  private ImmutableList<String> getExpandedStringListAttr(
-      String attributeName,
-      Tokenize tokenize) {
-    if (!getRule().isAttrDefined(attributeName, Type.STRING_LIST)) {
-      // TODO(bazel-team): This should be an error.
-      return ImmutableList.of();
-    }
-    List<String> original = attributes().get(attributeName, Type.STRING_LIST);
-    if (original.isEmpty()) {
-      return ImmutableList.of();
-    }
-    List<String> tokens = new ArrayList<>();
-    LocationExpander locationExpander =
-        new LocationExpander(this, LocationExpander.Options.ALLOW_DATA);
-
-    for (String token : original) {
-      expandValue(tokens, attributeName, token, locationExpander, tokenize);
-    }
-    return ImmutableList.copyOf(tokens);
-  }
-
-  /**
-   * Expands make variables in value and tokenizes the result into tokens.
-   */
-  public void tokenizeAndExpandMakeVars(
-      List<String> result,
-      String attributeName,
-      String value) {
-    LocationExpander locationExpander =
-        new LocationExpander(this, Options.ALLOW_DATA, Options.EXEC_PATHS);
-    expandValue(result, attributeName, value, locationExpander, Tokenize.YES);
-  }
-
-  /**
-   * Expands make variables and $(location) tags in value, and optionally tokenizes the result.
-   */
-  private void expandValue(
-      List<String> tokens,
-      String attributeName,
-      String value,
-      @Nullable LocationExpander locationExpander,
-      Tokenize tokenize) {
-    if (locationExpander != null) {
-      value = locationExpander.expandAttribute(attributeName, value);
-    }
-    value = expandMakeVariables(attributeName, value);
-    if (tokenize == Tokenize.YES) {
-      try {
-        ShellUtils.tokenize(tokens, value);
-      } catch (ShellUtils.TokenizationException e) {
-        attributeError(attributeName, e.getMessage());
-      }
-    } else {
-      tokens.add(value);
-    }
+  public Expander getExpander() {
+    return new Expander(this, getConfigurationMakeVariableContext());
   }
 
   public ImmutableMap<String, String> getMakeVariables(Iterable<String> attributeNames) {
@@ -1095,83 +1009,6 @@
     return configurationMakeVariableContext;
   }
 
-  /**
-   * Expands the make variables in {@code expression}.
-   *
-   * @param attributeName the name of the attribute from which "expression" comes; used for error
-   *     reporting.
-   * @return the expanded string.
-   */
-  public String expandedMakeVariables(String attributeName) {
-    String expression = attributes().get(attributeName, Type.STRING);
-    return expandMakeVariables(attributeName, expression);
-  }
-
-  /**
-   * Expands the make variables in {@code expression}.
-   *
-   * @param attributeName the name of the attribute from which "expression" comes; used for error
-   *     reporting.
-   * @param expression the string to expand.
-   * @return the expanded string.
-   */
-  public String expandMakeVariables(String attributeName, String expression) {
-    return expandMakeVariables(attributeName, expression, getConfigurationMakeVariableContext());
-  }
-
-  /**
-   * Returns the string "expression" after expanding all embedded references to
-   * "Make" variables.  If any errors are encountered, they are reported, and
-   * "expression" is returned unchanged.
-   *
-   * @param attributeName the name of the attribute from which "expression" comes;
-   *     used for error reporting.
-   * @param expression the string to expand.
-   * @param context the ConfigurationMakeVariableContext which can have a customized
-   *     lookupMakeVariable(String) method.
-   * @return the expansion of "expression".
-   */
-  public String expandMakeVariables(
-      String attributeName, String expression, ConfigurationMakeVariableContext context) {
-    try {
-      return MakeVariableExpander.expand(expression, context);
-    } catch (MakeVariableExpander.ExpansionException e) {
-      attributeError(attributeName, e.getMessage());
-      return expression;
-    }
-  }
-
-  /**
-   * Gets the value of the STRING_LIST attribute expanding all make variables.
-   */
-  public List<String> expandedMakeVariablesList(String attrName) {
-    List<String> variables = new ArrayList<>();
-    for (String variable : attributes().get(attrName, Type.STRING_LIST)) {
-      variables.add(expandMakeVariables(attrName, variable));
-    }
-    return variables;
-  }
-
-  /**
-   * If the string consists of a single variable, returns the expansion of that variable. Otherwise,
-   * returns null. Syntax errors are reported.
-   *
-   * @param attrName the name of the attribute from which "expression" comes; used for error
-   *     reporting.
-   * @param expression the string to expand.
-   * @return the expansion of "expression", or null.
-   */
-  @Nullable
-  public String expandSingleMakeVariable(String attrName, String expression) {
-    try {
-      return MakeVariableExpander
-          .expandSingleVariable(expression, getConfigurationMakeVariableContext());
-    } catch (MakeVariableExpander.ExpansionException e) {
-      attributeError(attrName, e.getMessage());
-      return expression;
-    }
-  }
-
   @Nullable
   public ToolchainContext getToolchainContext() {
     return toolchainContext;
diff --git a/src/main/java/com/google/devtools/build/lib/analysis/RunfilesSupport.java b/src/main/java/com/google/devtools/build/lib/analysis/RunfilesSupport.java
index 7f4c12d..230b1fd 100644
--- a/src/main/java/com/google/devtools/build/lib/analysis/RunfilesSupport.java
+++ b/src/main/java/com/google/devtools/build/lib/analysis/RunfilesSupport.java
@@ -428,6 +428,7 @@
       RuleContext ruleContext,
       CommandLine additionalArgs) {
     return CommandLine.concat(
-        ruleContext.getTokenizedStringListAttr("args"), additionalArgs);
+        ruleContext.getExpander().withDataLocations().tokenized("args"),
+        additionalArgs);
   }
 }
diff --git a/src/main/java/com/google/devtools/build/lib/analysis/skylark/SkylarkRuleContext.java b/src/main/java/com/google/devtools/build/lib/analysis/skylark/SkylarkRuleContext.java
index deb4595..fc82668 100644
--- a/src/main/java/com/google/devtools/build/lib/analysis/skylark/SkylarkRuleContext.java
+++ b/src/main/java/com/google/devtools/build/lib/analysis/skylark/SkylarkRuleContext.java
@@ -1019,9 +1019,7 @@
   public String expandMakeVariables(String attributeName, String command,
       final Map<String, String> additionalSubstitutions) throws EvalException {
     checkMutable("expand_make_variables");
-    return ruleContext.expandMakeVariables(
-        attributeName,
-        command,
+    ConfigurationMakeVariableContext makeVariableContext =
         new ConfigurationMakeVariableContext(
             // TODO(lberki): This should be removed. But only after either verifying that no one
             // uses it or providing an alternative.
@@ -1036,7 +1034,8 @@
               return super.lookupMakeVariable(variableName);
             }
           }
-        });
+        };
+    return ruleContext.getExpander(makeVariableContext).expand(attributeName, command);
   }
 
 
diff --git a/src/main/java/com/google/devtools/build/lib/bazel/rules/python/BazelPythonSemantics.java b/src/main/java/com/google/devtools/build/lib/bazel/rules/python/BazelPythonSemantics.java
index a11dbe2..328f553 100644
--- a/src/main/java/com/google/devtools/build/lib/bazel/rules/python/BazelPythonSemantics.java
+++ b/src/main/java/com/google/devtools/build/lib/bazel/rules/python/BazelPythonSemantics.java
@@ -101,7 +101,7 @@
     // adjusted to be relative to the workspace name.
     packageFragment = PathFragment.create(ruleContext.getWorkspaceName())
         .getRelative(packageFragment);
-    for (String importsAttr : ruleContext.expandedMakeVariablesList("imports")) {
+    for (String importsAttr : ruleContext.getExpander().list("imports")) {
       if (importsAttr.startsWith("/")) {
         ruleContext.attributeWarning("imports",
             "ignoring invalid absolute path '" + importsAttr + "'");
diff --git a/src/main/java/com/google/devtools/build/lib/rules/android/AndroidBinary.java b/src/main/java/com/google/devtools/build/lib/rules/android/AndroidBinary.java
index 39041a2..bfa8074 100644
--- a/src/main/java/com/google/devtools/build/lib/rules/android/AndroidBinary.java
+++ b/src/main/java/com/google/devtools/build/lib/rules/android/AndroidBinary.java
@@ -246,7 +246,7 @@
               resourceDeps,
               ruleContext.getImplicitOutputArtifact(AndroidRuleClasses.ANDROID_R_TXT),
               ResourceFilter.fromRuleContext(ruleContext),
-              ruleContext.getTokenizedStringListAttr("nocompress_extensions"),
+              ruleContext.getExpander().withDataLocations().tokenized("nocompress_extensions"),
               ruleContext.attributes().get("crunch_png", Type.BOOLEAN),
               ProguardHelper.getProguardConfigArtifact(ruleContext, ""),
               createMainDexProguardSpec(ruleContext),
@@ -266,7 +266,7 @@
                   ruleContext,
                   getDxArtifact(ruleContext, "android_instant_run.ap_"),
                   resourceDeps,
-                  ruleContext.getTokenizedStringListAttr("nocompress_extensions"),
+                  ruleContext.getExpander().withDataLocations().tokenized("nocompress_extensions"),
                   ruleContext.attributes().get("crunch_png", Type.BOOLEAN),
                   ProguardHelper.getProguardConfigArtifact(ruleContext, "instant_run"));
       ruleContext.assertNoErrors();
@@ -894,7 +894,7 @@
               .setTargetAaptVersion(AndroidAaptVersion.chooseTargetAaptVersion(ruleContext))
               .setResourceFilter(ResourceFilter.fromRuleContext(ruleContext))
               .setUncompressedExtensions(
-                  ruleContext.getTokenizedStringListAttr("nocompress_extensions"))
+                  ruleContext.getExpander().withDataLocations().tokenized("nocompress_extensions"))
               .build();
       filesBuilder.add(ruleContext.getImplicitOutputArtifact(
           AndroidRuleClasses.ANDROID_RESOURCE_SHRINKER_LOG));
@@ -938,7 +938,7 @@
       Function<Artifact, Artifact> derivedJarFunction,
       @Nullable  Artifact proguardOutputMap)
       throws InterruptedException, RuleErrorException {
-    List<String> dexopts = ruleContext.getTokenizedStringListAttr("dexopts");
+    List<String> dexopts = ruleContext.getExpander().withDataLocations().tokenized("dexopts");
     MultidexMode multidexMode = getMultidexMode(ruleContext);
     if (!supportsMultidexMode(ruleContext, multidexMode)) {
       ruleContext.throwWithRuleError("Multidex mode \"" + multidexMode.getAttributeValue()
@@ -1456,7 +1456,9 @@
                     .addExecPath(mainDexList)
                     .addExecPath(strippedJar)
                     .addExecPath(jar)
-                    .addAll(ruleContext.getTokenizedStringListAttr("main_dex_list_opts"))
+                    .addAll(
+                        ruleContext
+                            .getExpander().withDataLocations().tokenized("main_dex_list_opts"))
                     .build())
             .build(ruleContext));
     return mainDexList;
diff --git a/src/main/java/com/google/devtools/build/lib/rules/android/AndroidBinaryMobileInstall.java b/src/main/java/com/google/devtools/build/lib/rules/android/AndroidBinaryMobileInstall.java
index 4cde375..a8eb438 100644
--- a/src/main/java/com/google/devtools/build/lib/rules/android/AndroidBinaryMobileInstall.java
+++ b/src/main/java/com/google/devtools/build/lib/rules/android/AndroidBinaryMobileInstall.java
@@ -78,7 +78,7 @@
                   ruleContext.getImplicitOutputArtifact(
                       AndroidRuleClasses.ANDROID_INCREMENTAL_RESOURCES_APK),
                   resourceDeps,
-                  ruleContext.getTokenizedStringListAttr("nocompress_extensions"),
+                  ruleContext.getExpander().withDataLocations().tokenized("nocompress_extensions"),
                   ruleContext.attributes().get("crunch_png", Type.BOOLEAN),
                   ProguardHelper.getProguardConfigArtifact(ruleContext, "incremental"));
       ruleContext.assertNoErrors();
@@ -90,7 +90,7 @@
                   ruleContext,
                   getMobileInstallArtifact(ruleContext, "android_resources.ap_"),
                   resourceDeps,
-                  ruleContext.getTokenizedStringListAttr("nocompress_extensions"),
+                  ruleContext.getExpander().withDataLocations().tokenized("nocompress_extensions"),
                   ruleContext.attributes().get("crunch_png", Type.BOOLEAN),
                   ProguardHelper.getProguardConfigArtifact(ruleContext, "incremental_split"));
       ruleContext.assertNoErrors();
diff --git a/src/main/java/com/google/devtools/build/lib/rules/android/AndroidHostServiceFixture.java b/src/main/java/com/google/devtools/build/lib/rules/android/AndroidHostServiceFixture.java
index dbe0034..676a9ee 100644
--- a/src/main/java/com/google/devtools/build/lib/rules/android/AndroidHostServiceFixture.java
+++ b/src/main/java/com/google/devtools/build/lib/rules/android/AndroidHostServiceFixture.java
@@ -52,7 +52,7 @@
         .addNativeDeclaredProvider(
             new AndroidHostServiceFixtureInfoProvider(
                 executable.getExecutable(),
-                ruleContext.getTokenizedStringListAttr("service_names"),
+                ruleContext.getExpander().withDataLocations().tokenized("service_names"),
                 AndroidCommon.getSupportApks(ruleContext),
                 ruleContext.attributes().get("provides_test_args", Type.BOOLEAN),
                 ruleContext.attributes().get("daemon", Type.BOOLEAN)))
diff --git a/src/main/java/com/google/devtools/build/lib/rules/android/ApkActionsBuilder.java b/src/main/java/com/google/devtools/build/lib/rules/android/ApkActionsBuilder.java
index c7abbb8..8391c5e 100644
--- a/src/main/java/com/google/devtools/build/lib/rules/android/ApkActionsBuilder.java
+++ b/src/main/java/com/google/devtools/build/lib/rules/android/ApkActionsBuilder.java
@@ -332,7 +332,7 @@
     }
 
     ImmutableList<String> noCompressExtensions =
-        ruleContext.getTokenizedStringListAttr("nocompress_extensions");
+        ruleContext.getExpander().withDataLocations().tokenized("nocompress_extensions");
     if (!noCompressExtensions.isEmpty()) {
       compressedApkCommandLine.addAll("--nocompress_suffixes", noCompressExtensions);
       singleJarCommandLine.addAll("--nocompress_suffixes", noCompressExtensions);
diff --git a/src/main/java/com/google/devtools/build/lib/rules/android/ApplicationManifest.java b/src/main/java/com/google/devtools/build/lib/rules/android/ApplicationManifest.java
index e70bf42..bf89607 100644
--- a/src/main/java/com/google/devtools/build/lib/rules/android/ApplicationManifest.java
+++ b/src/main/java/com/google/devtools/build/lib/rules/android/ApplicationManifest.java
@@ -213,7 +213,7 @@
 
     for (String variable : manifestValues.keySet()) {
       manifestValues.put(
-          variable, context.expandMakeVariables("manifest_values", manifestValues.get(variable)));
+          variable, context.getExpander().expand("manifest_values", manifestValues.get(variable)));
     }
     return ImmutableMap.copyOf(manifestValues);
   }
@@ -805,7 +805,7 @@
     ResourceFilter resourceFilter = ResourceFilter.fromRuleContext(ruleContext);
 
     List<String> uncompressedExtensions =
-        ruleContext.getTokenizedStringListAttr("nocompress_extensions");
+        ruleContext.getExpander().withDataLocations().tokenized("nocompress_extensions");
 
     ImmutableList.Builder<String> additionalAaptOpts = ImmutableList.builder();
 
diff --git a/src/main/java/com/google/devtools/build/lib/rules/cpp/CcCommon.java b/src/main/java/com/google/devtools/build/lib/rules/cpp/CcCommon.java
index 36278ef..2bd0e14 100644
--- a/src/main/java/com/google/devtools/build/lib/rules/cpp/CcCommon.java
+++ b/src/main/java/com/google/devtools/build/lib/rules/cpp/CcCommon.java
@@ -353,7 +353,7 @@
     if (!ruleContext.getRule().isAttrDefined(NO_COPTS_ATTRIBUTE, Type.STRING)) {
       return null;
     }
-    String nocoptsAttr = ruleContext.expandedMakeVariables(NO_COPTS_ATTRIBUTE);
+    String nocoptsAttr = ruleContext.getExpander().expand(NO_COPTS_ATTRIBUTE);
     try {
       return Pattern.compile(nocoptsAttr);
     } catch (PatternSyntaxException e) {
@@ -388,7 +388,7 @@
    */
   public List<String> getDefines() {
     List<String> defines = new ArrayList<>();
-    for (String define : ruleContext.expandedMakeVariablesList(DEFINES_ATTRIBUTE)) {
+    for (String define : ruleContext.getExpander().list(DEFINES_ATTRIBUTE)) {
       List<String> tokens = new ArrayList<>();
       try {
         ShellUtils.tokenize(tokens, define);
@@ -447,7 +447,7 @@
     List<PathFragment> result = new ArrayList<>();
     PackageIdentifier packageIdentifier = ruleContext.getLabel().getPackageIdentifier();
     PathFragment packageFragment = packageIdentifier.getPathUnderExecRoot();
-    for (String includesAttr : ruleContext.expandedMakeVariablesList("includes")) {
+    for (String includesAttr : ruleContext.getExpander().list("includes")) {
       if (includesAttr.startsWith("/")) {
         ruleContext.attributeWarning("includes",
             "ignoring invalid absolute path '" + includesAttr + "'");
diff --git a/src/main/java/com/google/devtools/build/lib/rules/cpp/CppHelper.java b/src/main/java/com/google/devtools/build/lib/rules/cpp/CppHelper.java
index e5c871e..6a21ba0 100644
--- a/src/main/java/com/google/devtools/build/lib/rules/cpp/CppHelper.java
+++ b/src/main/java/com/google/devtools/build/lib/rules/cpp/CppHelper.java
@@ -27,6 +27,7 @@
 import com.google.devtools.build.lib.actions.MiddlemanFactory;
 import com.google.devtools.build.lib.actions.ParameterFile;
 import com.google.devtools.build.lib.analysis.AnalysisUtils;
+import com.google.devtools.build.lib.analysis.Expander;
 import com.google.devtools.build.lib.analysis.FileProvider;
 import com.google.devtools.build.lib.analysis.PlatformConfiguration;
 import com.google.devtools.build.lib.analysis.RuleConfiguredTargetBuilder;
@@ -155,23 +156,22 @@
         !ruleContext.getFeatures().contains("no_copts_tokenization");
 
     List<String> tokens = new ArrayList<>();
+    Expander expander = ruleContext.getExpander().withDataExecLocations();
     for (String token : input) {
-      try {
-        // Legacy behavior: tokenize all items.
-        if (tokenization) {
-          ruleContext.tokenizeAndExpandMakeVars(tokens, attributeName, token);
-        } else {
-          String exp =
-              ruleContext.expandSingleMakeVariable(attributeName, token);
-          if (exp != null) {
+      // Legacy behavior: tokenize all items.
+      if (tokenization) {
+        expander.tokenizeAndExpandMakeVars(tokens, attributeName, token);
+      } else {
+        String exp = expander.expandSingleMakeVariable(attributeName, token);
+        if (exp != null) {
+          try {
             ShellUtils.tokenize(tokens, exp);
-          } else {
-            tokens.add(
-                ruleContext.expandMakeVariables(attributeName, token));
+          } catch (ShellUtils.TokenizationException e) {
+            ruleContext.attributeError(attributeName, e.getMessage());
           }
+        } else {
+          tokens.add(expander.expand(attributeName, token));
         }
-      } catch (ShellUtils.TokenizationException e) {
-        ruleContext.attributeError(attributeName, e.getMessage());
       }
     }
     return ImmutableList.copyOf(tokens);
@@ -202,13 +202,14 @@
   public static List<String> expandLinkopts(
       RuleContext ruleContext, String attrName, Iterable<String> values) {
     List<String> result = new ArrayList<>();
+    Expander expander = ruleContext.getExpander().withDataExecLocations();
     for (String value : values) {
       if (isLinkoptLabel(value)) {
         if (!expandLabel(ruleContext, result, value)) {
           ruleContext.attributeError(attrName, "could not resolve label '" + value + "'");
         }
       } else {
-        ruleContext
+        expander
             .tokenizeAndExpandMakeVars(
                 result,
                 attrName,
diff --git a/src/main/java/com/google/devtools/build/lib/rules/genrule/GenRuleBase.java b/src/main/java/com/google/devtools/build/lib/rules/genrule/GenRuleBase.java
index f91bcde..2a59bff 100644
--- a/src/main/java/com/google/devtools/build/lib/rules/genrule/GenRuleBase.java
+++ b/src/main/java/com/google/devtools/build/lib/rules/genrule/GenRuleBase.java
@@ -270,10 +270,9 @@
    */
   protected String resolveCommand(String command, final RuleContext ruleContext,
       final NestedSet<Artifact> resolvedSrcs, final NestedSet<Artifact> filesToBuild) {
-    return ruleContext.expandMakeVariables(
-        "cmd",
-        command,
-        new CommandResolverContext(ruleContext, resolvedSrcs, filesToBuild));
+    return ruleContext
+        .getExpander(new CommandResolverContext(ruleContext, resolvedSrcs, filesToBuild))
+        .expand("cmd", command);
   }
 
   /**
diff --git a/src/main/java/com/google/devtools/build/lib/rules/java/JavaCommon.java b/src/main/java/com/google/devtools/build/lib/rules/java/JavaCommon.java
index b394ef6..489844c 100644
--- a/src/main/java/com/google/devtools/build/lib/rules/java/JavaCommon.java
+++ b/src/main/java/com/google/devtools/build/lib/rules/java/JavaCommon.java
@@ -461,7 +461,7 @@
     return Streams.concat(
             JavaToolchainProvider.fromRuleContext(ruleContext).getJavacOptions().stream(),
             Streams.stream(extraJavacOpts),
-            ruleContext.getTokenizedStringListAttr("javacopts").stream())
+            ruleContext.getExpander().withDataLocations().tokenized("javacopts").stream())
         .collect(toImmutableList());
   }
 
@@ -569,7 +569,7 @@
   public static List<String> getJvmFlags(RuleContext ruleContext) {
     List<String> jvmFlags = new ArrayList<>();
     jvmFlags.addAll(ruleContext.getFragment(JavaConfiguration.class).getDefaultJvmFlags());
-    jvmFlags.addAll(ruleContext.getExpandedStringListAttr("jvm_flags"));
+    jvmFlags.addAll(ruleContext.getExpander().withDataLocations().list("jvm_flags"));
     return jvmFlags;
   }
 
diff --git a/src/main/java/com/google/devtools/build/lib/rules/java/JavaCompilationHelper.java b/src/main/java/com/google/devtools/build/lib/rules/java/JavaCompilationHelper.java
index b1629f0..e235cd5 100644
--- a/src/main/java/com/google/devtools/build/lib/rules/java/JavaCompilationHelper.java
+++ b/src/main/java/com/google/devtools/build/lib/rules/java/JavaCompilationHelper.java
@@ -721,7 +721,7 @@
     return ImmutableList.copyOf(
         Iterables.concat(
             JavaToolchainProvider.fromRuleContext(ruleContext).getJavacOptions(),
-            ruleContext.getTokenizedStringListAttr("javacopts")));
+            ruleContext.getExpander().withDataLocations().tokenized("javacopts")));
   }
 
   public void setTranslations(Collection<Artifact> translations) {
diff --git a/src/main/java/com/google/devtools/build/lib/rules/java/JavaRuntime.java b/src/main/java/com/google/devtools/build/lib/rules/java/JavaRuntime.java
index 5f0cbdb..004ce3e 100644
--- a/src/main/java/com/google/devtools/build/lib/rules/java/JavaRuntime.java
+++ b/src/main/java/com/google/devtools/build/lib/rules/java/JavaRuntime.java
@@ -47,7 +47,7 @@
     PathFragment javaHome = defaultJavaHome(ruleContext.getLabel());
     if (ruleContext.attributes().isAttributeValueExplicitlySpecified("java_home")) {
       PathFragment javaHomeAttribute =
-          PathFragment.create(ruleContext.expandedMakeVariables("java_home"));
+          PathFragment.create(ruleContext.getExpander().expand("java_home"));
       if (!filesToBuild.isEmpty() && javaHomeAttribute.isAbsolute()) {
         ruleContext.ruleError("'java_home' with an absolute path requires 'srcs' to be empty.");
       }
diff --git a/src/main/java/com/google/devtools/build/lib/rules/java/JavaSkylarkCommon.java b/src/main/java/com/google/devtools/build/lib/rules/java/JavaSkylarkCommon.java
index 1c87c48..9e46ef9 100644
--- a/src/main/java/com/google/devtools/build/lib/rules/java/JavaSkylarkCommon.java
+++ b/src/main/java/com/google/devtools/build/lib/rules/java/JavaSkylarkCommon.java
@@ -512,7 +512,8 @@
         (ConfiguredTarget) skylarkRuleContext.getAttr().getValue(javaToolchainAttr);
     JavaToolchainProvider toolchain = getJavaToolchainProvider(javaToolchainConfigTarget);
     return ImmutableList.copyOf(Iterables.concat(
-        toolchain.getJavacOptions(), ruleContext.getTokenizedStringListAttr("javacopts")));
+        toolchain.getJavacOptions(),
+        ruleContext.getExpander().withDataLocations().tokenized("javacopts")));
   }
 
   @SkylarkCallable(
diff --git a/src/main/java/com/google/devtools/build/lib/rules/java/JavaToolchain.java b/src/main/java/com/google/devtools/build/lib/rules/java/JavaToolchain.java
index 06d6fa7..35102f3 100644
--- a/src/main/java/com/google/devtools/build/lib/rules/java/JavaToolchain.java
+++ b/src/main/java/com/google/devtools/build/lib/rules/java/JavaToolchain.java
@@ -54,7 +54,7 @@
         ruleContext, "extclasspath", Mode.HOST);
     String encoding = ruleContext.attributes().get("encoding", Type.STRING);
     List<String> xlint = ruleContext.attributes().get("xlint", Type.STRING_LIST);
-    List<String> misc = ruleContext.getTokenizedStringListAttr("misc");
+    List<String> misc = ruleContext.getExpander().withDataLocations().tokenized("misc");
     boolean javacSupportsWorkers =
         ruleContext.attributes().get("javac_supports_workers", Type.BOOLEAN);
     Artifact javac = ruleContext.getPrerequisiteArtifact("javac", Mode.HOST);
diff --git a/src/main/java/com/google/devtools/build/lib/rules/objc/AppleStubBinary.java b/src/main/java/com/google/devtools/build/lib/rules/objc/AppleStubBinary.java
index 2d027bc..43ea219 100644
--- a/src/main/java/com/google/devtools/build/lib/rules/objc/AppleStubBinary.java
+++ b/src/main/java/com/google/devtools/build/lib/rules/objc/AppleStubBinary.java
@@ -194,7 +194,8 @@
           AppleStubBinaryRule.XCENV_BASED_PATH_ATTR, PATH_NOT_NORMALIZED_ERROR);
     }
 
-    return ruleContext.expandMakeVariables(
-        AppleStubBinaryRule.XCENV_BASED_PATH_ATTR, pathString, makeVariableContext);
+    return ruleContext
+        .getExpander(makeVariableContext)
+        .expand(AppleStubBinaryRule.XCENV_BASED_PATH_ATTR, pathString);
   }
 }
diff --git a/src/main/java/com/google/devtools/build/lib/rules/objc/CompilationAttributes.java b/src/main/java/com/google/devtools/build/lib/rules/objc/CompilationAttributes.java
index 6700f31..39c28b7 100644
--- a/src/main/java/com/google/devtools/build/lib/rules/objc/CompilationAttributes.java
+++ b/src/main/java/com/google/devtools/build/lib/rules/objc/CompilationAttributes.java
@@ -253,11 +253,11 @@
 
     private static void addCompileOptionsFromRuleContext(Builder builder, RuleContext ruleContext) {
       if (ruleContext.attributes().has("copts", Type.STRING_LIST)) {
-        builder.addCopts(ruleContext.getTokenizedStringListAttr("copts"));
+        builder.addCopts(ruleContext.getExpander().withDataLocations().tokenized("copts"));
       }
 
       if (ruleContext.attributes().has("linkopts", Type.STRING_LIST)) {
-        builder.addLinkopts(ruleContext.getTokenizedStringListAttr("linkopts"));
+        builder.addLinkopts(ruleContext.getExpander().withDataLocations().tokenized("linkopts"));
       }
     }
 
diff --git a/src/main/java/com/google/devtools/build/lib/rules/objc/IosTest.java b/src/main/java/com/google/devtools/build/lib/rules/objc/IosTest.java
index b6561b1..9b5d87a 100644
--- a/src/main/java/com/google/devtools/build/lib/rules/objc/IosTest.java
+++ b/src/main/java/com/google/devtools/build/lib/rules/objc/IosTest.java
@@ -260,7 +260,7 @@
                 CompilationAttributes.Builder.fromRuleContext(ruleContext).build())
             .setCompilationArtifacts(compilationArtifacts)
             .setResourceAttributes(new ResourceAttributes(ruleContext))
-            .addDefines(ruleContext.getTokenizedStringListAttr("defines"))
+            .addDefines(ruleContext.getExpander().withDataLocations().tokenized("defines"))
             .addDeps(ruleContext.getPrerequisites("deps", Mode.TARGET))
             .addRuntimeDeps(ruleContext.getPrerequisites("runtime_deps", Mode.TARGET))
             .addDeps(ruleContext.getPrerequisites("bundles", Mode.TARGET))
diff --git a/src/main/java/com/google/devtools/build/lib/rules/objc/ObjcCommon.java b/src/main/java/com/google/devtools/build/lib/rules/objc/ObjcCommon.java
index f6b0efe..c104bf0 100644
--- a/src/main/java/com/google/devtools/build/lib/rules/objc/ObjcCommon.java
+++ b/src/main/java/com/google/devtools/build/lib/rules/objc/ObjcCommon.java
@@ -645,7 +645,7 @@
   static Iterable<String> getNonCrosstoolCopts(RuleContext ruleContext) {
     return Iterables.concat(
         ruleContext.getFragment(ObjcConfiguration.class).getCopts(),
-        ruleContext.getTokenizedStringListAttr("copts"));
+        ruleContext.getExpander().withDataLocations().tokenized("copts"));
   }
 
   static ImmutableSet<PathFragment> userHeaderSearchPaths(
diff --git a/src/main/java/com/google/devtools/build/lib/rules/objc/ObjcLibrary.java b/src/main/java/com/google/devtools/build/lib/rules/objc/ObjcLibrary.java
index b93baea..be449de 100644
--- a/src/main/java/com/google/devtools/build/lib/rules/objc/ObjcLibrary.java
+++ b/src/main/java/com/google/devtools/build/lib/rules/objc/ObjcLibrary.java
@@ -41,7 +41,7 @@
         .setCompilationAttributes(
             CompilationAttributes.Builder.fromRuleContext(ruleContext).build())
         .setResourceAttributes(new ResourceAttributes(ruleContext))
-        .addDefines(ruleContext.getTokenizedStringListAttr("defines"))
+        .addDefines(ruleContext.getExpander().withDataLocations().tokenized("defines"))
         .setCompilationArtifacts(CompilationSupport.compilationArtifacts(ruleContext))
         .addDeps(ruleContext.getPrerequisites("deps", Mode.TARGET))
         .addRuntimeDeps(ruleContext.getPrerequisites("runtime_deps", Mode.TARGET))