| // Copyright 2021 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.cmdline; | |
| import com.google.auto.value.AutoValue; | |
| import com.google.common.annotations.VisibleForTesting; | |
| import com.google.devtools.build.lib.util.StringUtilities; | |
| import com.google.errorprone.annotations.FormatMethod; | |
| import javax.annotation.Nullable; | |
| /** Utilities to help parse labels. */ | |
| final class LabelParser { | |
| private LabelParser() {} | |
| /** | |
| * Contains the parsed elements of a label string. The parts are validated (they don't contain | |
| * invalid characters). See {@link #parse} for valid label patterns. | |
| */ | |
| @AutoValue | |
| abstract static class Parts { | |
| /** | |
| * The {@code @repo} or {@code @@canonical_repo} part of the string (sans any leading | |
| * {@literal @}s); can be null if it doesn't have such a part (i.e. if it doesn't start with a | |
| * {@literal @}). | |
| */ | |
| @Nullable | |
| abstract String repo(); | |
| /** | |
| * Whether the repo part is using the canonical repo syntax (two {@literal @}s) or not (one | |
| * {@literal @}). If there is no repo part, this is false. | |
| */ | |
| abstract boolean repoIsCanonical(); | |
| /** | |
| * Whether the package part of the string is prefixed by double-slash. This can only be false if | |
| * the repo part is missing. | |
| */ | |
| abstract boolean pkgIsAbsolute(); | |
| /** | |
| * The package part of the string (sans the leading double-slash, if present; also sans the | |
| * final '...' segment, if present). | |
| */ | |
| abstract String pkg(); | |
| /** Whether the package part of the string ends with a '...' segment. */ | |
| abstract boolean pkgEndsWithTripleDots(); | |
| /** The target part of the string (sans colon). */ | |
| abstract String target(); | |
| /** The original unparsed raw string. */ | |
| abstract String raw(); | |
| @VisibleForTesting | |
| static Parts validateAndCreate( | |
| @Nullable String repo, | |
| boolean repoIsCanonical, | |
| boolean pkgIsAbsolute, | |
| String pkg, | |
| boolean pkgEndsWithTripleDots, | |
| String target, | |
| String raw) | |
| throws LabelSyntaxException { | |
| validateRepoName(repo); | |
| validatePackageName(pkg, target); | |
| return new AutoValue_LabelParser_Parts( | |
| repo, | |
| repoIsCanonical, | |
| pkgIsAbsolute, | |
| pkg, | |
| pkgEndsWithTripleDots, | |
| validateAndProcessTargetName(pkg, target, pkgEndsWithTripleDots), | |
| raw); | |
| } | |
| /** | |
| * Parses a raw label string into parts. The logic can be summarized by the following table: | |
| * | |
| * <pre>{@code | |
| * raw | repo | repoIs- | pkgIs- | pkg | pkgEndsWith- | target | |
| * | | Canonical | Absolute | | TripleDots | | |
| * ----------------------+--------+-----------+----------+-----------+--------------+----------- | |
| * "foo/bar" | null | false | false | "" | false | "foo/bar" | |
| * "..." | null | false | false | "" | true | "" | |
| * "...:all" | null | false | false | "" | true | "all" | |
| * "foo/..." | null | false | false | "foo" | true | "" | |
| * "//foo/bar" | null | false | true | "foo/bar" | false | "bar" | |
| * "//foo/..." | null | false | true | "foo" | true | "" | |
| * "//foo/...:all" | null | false | true | "foo" | true | "all" | |
| * "//foo/all" | null | false | true | "foo/all" | false | "all" | |
| * "@repo" | "repo" | false | true | "" | false | "repo" | |
| * "@@repo" | "repo" | true | true | "" | false | "repo" | |
| * "@repo//foo/bar" | "repo" | false | true | "foo/bar" | false | "bar" | |
| * "@@repo//foo/bar" | "repo" | true | true | "foo/bar" | false | "bar" | |
| * ":quux" | null | false | false | "" | false | "quux" | |
| * "foo/bar:quux" | null | false | false | "foo/bar" | false | "quux" | |
| * "//foo/bar:quux" | null | false | true | "foo/bar" | false | "quux" | |
| * "@repo//foo/bar:quux" | "repo" | false | true | "foo/bar" | false | "quux" | |
| * }</pre> | |
| */ | |
| static Parts parse(String rawLabel) throws LabelSyntaxException { | |
| @Nullable final String repo; | |
| final boolean repoIsCanonical = rawLabel.startsWith("@@"); | |
| final int startOfPackage; | |
| final int doubleSlashIndex = rawLabel.indexOf("//"); | |
| final boolean pkgIsAbsolute; | |
| if (rawLabel.startsWith("@")) { | |
| if (doubleSlashIndex < 0) { | |
| // Special case: the label "@foo" is synonymous with "@foo//:foo". | |
| repo = rawLabel.substring(repoIsCanonical ? 2 : 1); | |
| return validateAndCreate( | |
| repo, | |
| repoIsCanonical, | |
| /* pkgIsAbsolute= */ true, | |
| /* pkg= */ "", | |
| /* pkgEndsWithTripleDots= */ false, | |
| /* target= */ repo, | |
| rawLabel); | |
| } else { | |
| repo = rawLabel.substring(repoIsCanonical ? 2 : 1, doubleSlashIndex); | |
| startOfPackage = doubleSlashIndex + 2; | |
| pkgIsAbsolute = true; | |
| } | |
| } else { | |
| // If the label begins with '//', it's an absolute label. Otherwise, treat it as relative | |
| // (the command-line kind). | |
| pkgIsAbsolute = doubleSlashIndex == 0; | |
| startOfPackage = doubleSlashIndex == 0 ? 2 : 0; | |
| repo = null; | |
| } | |
| final String pkg; | |
| final String target; | |
| final int colonIndex = rawLabel.indexOf(':', startOfPackage); | |
| final String rawPkg = | |
| rawLabel.substring(startOfPackage, colonIndex >= 0 ? colonIndex : rawLabel.length()); | |
| final boolean pkgEndsWithTripleDots = rawPkg.endsWith("/...") || rawPkg.equals("..."); | |
| if (colonIndex < 0 && pkgEndsWithTripleDots) { | |
| // Special case: if the entire label ends in '...', the target name is empty. | |
| pkg = stripTrailingTripleDots(rawPkg); | |
| target = ""; | |
| } else if (colonIndex < 0 && !pkgIsAbsolute) { | |
| // Special case: the label "foo/bar" is synonymous with ":foo/bar". | |
| pkg = ""; | |
| target = rawLabel.substring(startOfPackage); | |
| } else { | |
| pkg = stripTrailingTripleDots(rawPkg); | |
| if (colonIndex >= 0) { | |
| target = rawLabel.substring(colonIndex + 1); | |
| } else { | |
| // Special case: the label "[@repo]//foo/bar" is synonymous with "[@repo]//foo/bar:bar". | |
| // The target name is the last package segment (works even if `pkg` contains no slash) | |
| target = pkg.substring(pkg.lastIndexOf('/') + 1); | |
| } | |
| } | |
| return validateAndCreate( | |
| repo, repoIsCanonical, pkgIsAbsolute, pkg, pkgEndsWithTripleDots, target, rawLabel); | |
| } | |
| private static String stripTrailingTripleDots(String pkg) { | |
| if (pkg.endsWith("/...")) { | |
| return pkg.substring(0, pkg.length() - 4); | |
| } | |
| if (pkg.equals("...")) { | |
| return ""; | |
| } | |
| return pkg; | |
| } | |
| private static void validateRepoName(@Nullable String repo) throws LabelSyntaxException { | |
| if (repo != null) { | |
| RepositoryName.validate(repo); | |
| } | |
| } | |
| private static void validatePackageName(String pkg, String target) throws LabelSyntaxException { | |
| String pkgError = LabelValidator.validatePackageName(pkg); | |
| if (pkgError != null) { | |
| throw syntaxErrorf( | |
| "invalid package name '%s': %s%s", pkg, pkgError, perhapsYouMeantMessage(pkg, target)); | |
| } | |
| } | |
| void checkPkgIsAbsolute() throws LabelSyntaxException { | |
| if (!pkgIsAbsolute()) { | |
| throw syntaxErrorf("invalid label '%s': absolute label must begin with '@' or '//'", raw()); | |
| } | |
| } | |
| void checkPkgDoesNotEndWithTripleDots() throws LabelSyntaxException { | |
| if (pkgEndsWithTripleDots()) { | |
| throw syntaxErrorf("invalid label '%s': package name cannot contain '...'", raw()); | |
| } | |
| } | |
| } | |
| @FormatMethod | |
| static LabelSyntaxException syntaxErrorf(String format, Object... args) { | |
| return new LabelSyntaxException( | |
| StringUtilities.sanitizeControlChars(String.format(format, args))); | |
| } | |
| private static String perhapsYouMeantMessage(String pkg, String target) { | |
| return pkg.endsWith('/' + target) ? " (perhaps you meant \":" + target + "\"?)" : ""; | |
| } | |
| static String validateAndProcessTargetName( | |
| String pkg, String target, boolean pkgEndsWithTripleDots) throws LabelSyntaxException { | |
| if (pkgEndsWithTripleDots && target.isEmpty()) { | |
| // Allow empty target name if the package part ends in '...'. | |
| return target; | |
| } | |
| String targetError = LabelValidator.validateTargetName(target); | |
| if (targetError != null) { | |
| throw syntaxErrorf( | |
| "invalid target name '%s': %s%s", | |
| target, targetError, perhapsYouMeantMessage(pkg, target)); | |
| } | |
| // TODO(bazel-team): This should be an error, but we can't make it one for legacy reasons. | |
| if (target.endsWith("/.")) { | |
| return target.substring(0, target.length() - 2); | |
| } | |
| return target; | |
| } | |
| } |