| // 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.ImmutableMap; | |
| 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)) | |
| .precomputed(); | |
| /** | |
| * 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.getRelativeWithRemapping(labelText, ImmutableMap.of()); | |
| } 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; | |
| } | |
| } | |
| } |