blob: c741275dc3411a69e35d9544b3653c11cae8b988 [file] [log] [blame]
Damien Martin-Guillerezf88f4d82015-09-25 13:56:55 +00001// Copyright 2014 The Bazel Authors. All rights reserved.
Han-Wen Nienhuysd08b27f2015-02-25 16:45:20 +01002//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7// http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15package com.google.devtools.build.lib.analysis;
16
17import com.google.common.base.CharMatcher;
tomlua155b532017-11-08 20:12:47 +010018import com.google.common.base.Preconditions;
Han-Wen Nienhuysd08b27f2015-02-25 16:45:20 +010019import com.google.common.base.Strings;
dannarka8d73572018-06-21 17:46:06 -070020import com.google.common.collect.ImmutableMap;
Han-Wen Nienhuysd08b27f2015-02-25 16:45:20 +010021import com.google.common.collect.Iterables;
22import com.google.devtools.build.lib.actions.Artifact;
Lukacs Berki6e91eb92015-09-21 09:12:37 +000023import com.google.devtools.build.lib.cmdline.Label;
Lukacs Berkia6434362015-09-15 13:56:14 +000024import com.google.devtools.build.lib.cmdline.LabelSyntaxException;
Han-Wen Nienhuysd08b27f2015-02-25 16:45:20 +010025import com.google.devtools.build.lib.vfs.PathFragment;
Han-Wen Nienhuysd08b27f2015-02-25 16:45:20 +010026import java.util.ArrayList;
27import java.util.List;
28import java.util.Map;
Han-Wen Nienhuysd08b27f2015-02-25 16:45:20 +010029import javax.annotation.Nullable;
30
31/**
32 * Helper class encapsulating string scanning state used during "heuristic"
33 * expansion of labels embedded within rules.
34 */
35public final class LabelExpander {
36 /**
37 * An exception that is thrown when a label is expanded to zero or multiple
38 * files during expansion.
39 */
40 public static class NotUniqueExpansionException extends Exception {
41 public NotUniqueExpansionException(int sizeOfResultSet, String labelText) {
42 super("heuristic label expansion found '" + labelText + "', which expands to "
43 + sizeOfResultSet + " files"
44 + (sizeOfResultSet > 1
45 ? ", please use $(locations " + labelText + ") instead"
46 : ""));
47 }
48 }
49
50 // This is a utility class, no need to instantiate.
51 private LabelExpander() {}
52
53 /**
54 * CharMatcher to determine if a given character is valid for labels.
55 *
56 * <p>The Build Concept Reference additionally allows '=' and ',' to appear in labels,
57 * but for the purposes of the heuristic, this function does not, as it would cause
58 * "--foo=:rule1,:rule2" to scan as a single possible label, instead of three
59 * ("--foo", ":rule1", ":rule2").
60 */
61 private static final CharMatcher LABEL_CHAR_MATCHER =
62 CharMatcher.inRange('a', 'z')
63 .or(CharMatcher.inRange('A', 'Z'))
64 .or(CharMatcher.inRange('0', '9'))
Googler7cf24602016-07-25 15:36:55 +000065 .or(CharMatcher.anyOf(":/_.-+" + PathFragment.SEPARATOR_CHAR))
66 .precomputed();
Han-Wen Nienhuysd08b27f2015-02-25 16:45:20 +010067
68 /**
69 * Expands all references to labels embedded within a string using the
70 * provided expansion mapping from labels to artifacts.
71 *
72 * <p>Since this pass is heuristic, references to non-existent labels (such
73 * as arbitrary words) or invalid labels are simply ignored and are unchanged
74 * in the output. However, if the heuristic discovers a label, which
75 * identifies an existing target producing zero or multiple files, an error
76 * is reported.
77 *
78 * @param expression the expression to expand.
79 * @param labelMap the mapping from labels to artifacts, whose relative path
80 * is to be used as the expansion.
81 * @param labelResolver the {@code Label} that can resolve label strings
82 * to {@code Label} objects. The resolved label is either relative to
83 * {@code labelResolver} or is a global label (i.e. starts with "//").
84 * @return the expansion of the string.
85 * @throws NotUniqueExpansionException if a label that is present in the
86 * mapping expands to zero or multiple files.
87 */
88 public static <T extends Iterable<Artifact>> String expand(@Nullable String expression,
89 Map<Label, T> labelMap, Label labelResolver) throws NotUniqueExpansionException {
90 if (Strings.isNullOrEmpty(expression)) {
91 return "";
92 }
93 Preconditions.checkNotNull(labelMap);
94 Preconditions.checkNotNull(labelResolver);
95
96 int offset = 0;
97 StringBuilder result = new StringBuilder();
98 while (offset < expression.length()) {
99 String labelText = scanLabel(expression, offset);
100 if (labelText != null) {
101 offset += labelText.length();
102 result.append(tryResolvingLabelTextToArtifactPath(labelText, labelMap, labelResolver));
103 } else {
104 result.append(expression.charAt(offset));
105 offset++;
106 }
107 }
108 return result.toString();
109 }
110
111 /**
112 * Tries resolving a label text to a full label for the associated {@code
113 * Artifact}, using the provided mapping.
114 *
115 * <p>The method succeeds if the label text can be resolved to a {@code
116 * Label} object, which is present in the {@code labelMap} and maps to
117 * exactly one {@code Artifact}.
118 *
119 * @param labelText the text to resolve.
120 * @param labelMap the mapping from labels to artifacts, whose relative path
121 * is to be used as the expansion.
122 * @param labelResolver the {@code Label} that can resolve label strings
123 * to {@code Label} objects. The resolved label is either relative to
124 * {@code labelResolver} or is a global label (i.e. starts with "//").
125 * @return an absolute label to an {@code Artifact} if the resolving was
126 * successful or the original label text.
127 * @throws NotUniqueExpansionException if a label that is present in the
128 * mapping expands to zero or multiple files.
129 */
130 private static <T extends Iterable<Artifact>> String tryResolvingLabelTextToArtifactPath(
131 String labelText, Map<Label, T> labelMap, Label labelResolver)
132 throws NotUniqueExpansionException {
133 Label resolvedLabel = resolveLabelText(labelText, labelResolver);
134 if (resolvedLabel != null) {
135 Iterable<Artifact> artifacts = labelMap.get(resolvedLabel);
136 if (artifacts != null) { // resolvedLabel identifies an existing target
137 List<String> locations = new ArrayList<>();
138 Artifact.addExecPaths(artifacts, locations);
139 int resultSetSize = locations.size();
140 if (resultSetSize == 1) {
141 return Iterables.getOnlyElement(locations); // success!
142 } else {
143 throw new NotUniqueExpansionException(resultSetSize, labelText);
144 }
145 }
146 }
147 return labelText;
148 }
149
150 /**
151 * Resolves a string to a label text. Uses {@code labelResolver} to do so.
152 * The result is either relative to {@code labelResolver} or is an absolute
153 * label. In case of an invalid label text, the return value is null.
154 */
155 private static Label resolveLabelText(String labelText, Label labelResolver) {
156 try {
dannarka8d73572018-06-21 17:46:06 -0700157 return labelResolver.getRelativeWithRemapping(labelText, ImmutableMap.of());
Lukacs Berkia6434362015-09-15 13:56:14 +0000158 } catch (LabelSyntaxException e) {
Han-Wen Nienhuysd08b27f2015-02-25 16:45:20 +0100159 // It's a heuristic, so quietly ignore "errors". Because Label.getRelative never
160 // returns null, we can use null to indicate an error.
161 return null;
162 }
163 }
164
165 /**
166 * Scans the argument string from a given start position until the name of a
167 * potential label has been consumed, then returns the label text. If
168 * the expression contains no possible label starting at the start position,
169 * the return value is null.
170 */
171 private static String scanLabel(String expression, int start) {
172 int offset = start;
173 while (offset < expression.length() && LABEL_CHAR_MATCHER.matches(expression.charAt(offset))) {
174 ++offset;
175 }
176 if (offset > start) {
177 return expression.substring(start, offset);
178 } else {
179 return null;
180 }
181 }
182}