aboutsummaryrefslogtreecommitdiffhomepage
path: root/src/main/java/com/google/devtools/build/lib/analysis/LabelExpander.java
blob: aa3cc94f69aaee66d519fed8f9315312fc7e7f37 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
// Copyright 2014 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.base.CharMatcher;
import com.google.common.base.Preconditions;
import com.google.common.base.Strings;
import com.google.common.collect.Iterables;
import com.google.devtools.build.lib.actions.Artifact;
import com.google.devtools.build.lib.cmdline.Label;
import com.google.devtools.build.lib.cmdline.LabelSyntaxException;
import com.google.devtools.build.lib.vfs.PathFragment;

import java.util.ArrayList;
import java.util.List;
import java.util.Map;

import javax.annotation.Nullable;

/**
 * Helper class encapsulating string scanning state used during "heuristic"
 * expansion of labels embedded within rules.
 */
public final class LabelExpander {
  /**
   * An exception that is thrown when a label is expanded to zero or multiple
   * files during expansion.
   */
  public static class NotUniqueExpansionException extends Exception {
    public NotUniqueExpansionException(int sizeOfResultSet, String labelText) {
      super("heuristic label expansion found '" + labelText + "', which expands to "
          + sizeOfResultSet + " files"
          + (sizeOfResultSet > 1
              ? ", please use $(locations " + labelText + ") instead"
              : ""));
    }
  }

  // This is a utility class, no need to instantiate.
  private LabelExpander() {}

  /**
   * CharMatcher to determine if a given character is valid for labels.
   *
   * <p>The Build Concept Reference additionally allows '=' and ',' to appear in labels,
   * but for the purposes of the heuristic, this function does not, as it would cause
   * "--foo=:rule1,:rule2" to scan as a single possible label, instead of three
   * ("--foo", ":rule1", ":rule2").
   */
  private static final CharMatcher LABEL_CHAR_MATCHER =
      CharMatcher.inRange('a', 'z')
      .or(CharMatcher.inRange('A', 'Z'))
      .or(CharMatcher.inRange('0', '9'))
      .or(CharMatcher.anyOf(":/_.-+" + PathFragment.SEPARATOR_CHAR));

  /**
   * Expands all references to labels embedded within a string using the
   * provided expansion mapping from labels to artifacts.
   *
   * <p>Since this pass is heuristic, references to non-existent labels (such
   * as arbitrary words) or invalid labels are simply ignored and are unchanged
   * in the output. However, if the heuristic discovers a label, which
   * identifies an existing target producing zero or multiple files, an error
   * is reported.
   *
   * @param expression the expression to expand.
   * @param labelMap the mapping from labels to artifacts, whose relative path
   *     is to be used as the expansion.
   * @param labelResolver the {@code Label} that can resolve label strings
   *     to {@code Label} objects. The resolved label is either relative to
   *     {@code labelResolver} or is a global label (i.e. starts with "//").
   * @return the expansion of the string.
   * @throws NotUniqueExpansionException if a label that is present in the
   *     mapping expands to zero or multiple files.
   */
  public static <T extends Iterable<Artifact>> String expand(@Nullable String expression,
      Map<Label, T> labelMap, Label labelResolver) throws NotUniqueExpansionException {
    if (Strings.isNullOrEmpty(expression)) {
      return "";
    }
    Preconditions.checkNotNull(labelMap);
    Preconditions.checkNotNull(labelResolver);

    int offset = 0;
    StringBuilder result = new StringBuilder();
    while (offset < expression.length()) {
      String labelText = scanLabel(expression, offset);
      if (labelText != null) {
        offset += labelText.length();
        result.append(tryResolvingLabelTextToArtifactPath(labelText, labelMap, labelResolver));
      } else {
        result.append(expression.charAt(offset));
        offset++;
      }
    }
    return result.toString();
  }

  /**
   * Tries resolving a label text to a full label for the associated {@code
   * Artifact}, using the provided mapping.
   *
   * <p>The method succeeds if the label text can be resolved to a {@code
   * Label} object, which is present in the {@code labelMap} and maps to
   * exactly one {@code Artifact}.
   *
   * @param labelText the text to resolve.
   * @param labelMap the mapping from labels to artifacts, whose relative path
   *     is to be used as the expansion.
   * @param labelResolver the {@code Label} that can resolve label strings
   *     to {@code Label} objects. The resolved label is either relative to
   *     {@code labelResolver} or is a global label (i.e. starts with "//").
   * @return an absolute label to an {@code Artifact} if the resolving was
   *     successful or the original label text.
   * @throws NotUniqueExpansionException if a label that is present in the
   *     mapping expands to zero or multiple files.
   */
  private static <T extends Iterable<Artifact>> String tryResolvingLabelTextToArtifactPath(
      String labelText, Map<Label, T> labelMap, Label labelResolver)
      throws NotUniqueExpansionException {
    Label resolvedLabel = resolveLabelText(labelText, labelResolver);
    if (resolvedLabel != null) {
      Iterable<Artifact> artifacts = labelMap.get(resolvedLabel);
      if (artifacts != null) { // resolvedLabel identifies an existing target
        List<String> locations = new ArrayList<>();
        Artifact.addExecPaths(artifacts, locations);
        int resultSetSize = locations.size();
        if (resultSetSize == 1) {
          return Iterables.getOnlyElement(locations); // success!
        } else {
          throw new NotUniqueExpansionException(resultSetSize, labelText);
        }
      }
    }
    return labelText;
  }

  /**
   * Resolves a string to a label text. Uses {@code labelResolver} to do so.
   * The result is either relative to {@code labelResolver} or is an absolute
   * label. In case of an invalid label text, the return value is null.
   */
  private static Label resolveLabelText(String labelText, Label labelResolver) {
    try {
      return labelResolver.getRelative(labelText);
    } catch (LabelSyntaxException e) {
      // It's a heuristic, so quietly ignore "errors". Because Label.getRelative never
      // returns null, we can use null to indicate an error.
      return null;
    }
  }

  /**
   * Scans the argument string from a given start position until the name of a
   * potential label has been consumed, then returns the label text. If
   * the expression contains no possible label starting at the start position,
   * the return value is null.
   */
  private static String scanLabel(String expression, int start) {
    int offset = start;
    while (offset < expression.length() && LABEL_CHAR_MATCHER.matches(expression.charAt(offset))) {
      ++offset;
    }
    if (offset > start) {
      return expression.substring(start, offset);
    } else {
      return null;
    }
  }
}