Skip to content

Commit

Permalink
Remove the concept of a "default" repo name and consolidate label par…
Browse files Browse the repository at this point in the history
…sing logic

This CL started as an effort to clean up references to RepositoryName.DEFAULT. This constant signifies that a label starts without a `@repo` part, and so usually refers to the "current repo". A Label or PackageIdentifier with such a repo name is unusable. This constant is really only ever meaningfully used *while* parsing a label to be relative from another one (Label#getRelativeWithMapping). So we remove it.

This also necessarily meant that we have to clean up the label parsing logic. There are multiple places in the Label, PackageIdentifier, and LabelValidator classes where the raw label string is parsed in some way and often they call each other, making the code nigh impossible to follow. This CL consolidates all the label parsing logic into one class, LabelParser, and makes all Label factory methods use this parser instead of their own logic.

PiperOrigin-RevId: 415235412
  • Loading branch information
Wyverald authored and copybara-github committed Dec 9, 2021
1 parent d7f0724 commit 103ea9d
Show file tree
Hide file tree
Showing 47 changed files with 386 additions and 406 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -679,10 +679,8 @@ private <T> void resolveAttribute(
return;
}

Label ruleLabel = rule.getLabel();
type.visitLabels(
(depLabel, ctx) ->
outgoingLabels.put(dependencyKind, ruleLabel.resolveRepositoryRelative(depLabel)),
(depLabel, ctx) -> outgoingLabels.put(dependencyKind, depLabel),
attributeValue,
/*context=*/ null);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -256,7 +256,7 @@ private ArtifactRoot buildDerivedRoot(
// instead. However, it requires individually symlinking the top-level elements of external
// repositories, which is blocked by a Windows symlink issue #8704.
RootType rootType;
if (repository.isMain() || repository.isDefault()) {
if (repository.isMain()) {
rootType = isMiddleman ? RootType.SiblingMainMiddleman : RootType.SiblingMainOutput;
} else {
rootType = isMiddleman ? RootType.SiblingExternalMiddleman : RootType.SiblingExternalOutput;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,6 @@ public Label load(String from) throws Exception {
try {
return Label.parseAbsolute(
from,
/* defaultToMain=*/ false,
/* repositoryMapping= */ ImmutableMap.of());
} catch (LabelSyntaxException e) {
throw new Exception(from);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -364,7 +364,7 @@ public ImmutableList<Path> plantSymlinkForest() throws IOException, AbruptExitEx
continue;
}
RepositoryName repository = pkgId.getRepository();
if (repository.isMain() || repository.isDefault()) {
if (repository.isMain()) {
// Record main repo packages.
packageRootsForMainRepo.put(entry.getKey(), entry.getValue());

Expand Down
1 change: 1 addition & 0 deletions src/main/java/com/google/devtools/build/lib/cmdline/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ java_library(
name = "cmdline-primitives",
srcs = [
"LabelConstants.java",
"LabelParser.java",
"LabelSyntaxException.java",
"PackageIdentifier.java",
"RepositoryMapping.java",
Expand Down
308 changes: 106 additions & 202 deletions src/main/java/com/google/devtools/build/lib/cmdline/Label.java

Large diffs are not rendered by default.

195 changes: 195 additions & 0 deletions src/main/java/com/google/devtools/build/lib/cmdline/LabelParser.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,195 @@
// 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.common.collect.ImmutableSet;
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 {
/**
* Package names that aren't made relative to the current repository because they mean special
* things to Bazel.
*/
private static final ImmutableSet<String> ABSOLUTE_PACKAGE_NAMES =
ImmutableSet.of(
// Used for select's `//conditions:default` label (not a target)
"conditions",
// Used for the public and private visibility labels (not targets)
"visibility",
// There is only one //external package
LabelConstants.EXTERNAL_PACKAGE_NAME.getPathString());

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.
*/
static final class Parts {
/**
* The {@code @repo} part of the string (sans the {@literal @}); can be null if it doesn't have
* such a part.
*/
@Nullable final String repo;
/** Whether the package part of the string is prefixed by double-slash. */
final boolean pkgIsAbsolute;
/** The package part of the string (sans double-slash, if any). */
final String pkg;
/** The target part of the string (sans colon). */
final String target;
/** The original unparsed raw string. */
final String raw;

private Parts(
@Nullable String repo, boolean pkgIsAbsolute, String pkg, String target, String raw) {
this.repo = repo;
this.pkgIsAbsolute = pkgIsAbsolute;
this.pkg = pkg;
this.target = target;
this.raw = raw;
}

private static Parts validateAndCreate(
@Nullable String repo, boolean pkgIsAbsolute, String pkg, String target, String raw)
throws LabelSyntaxException {
return new Parts(
validateAndProcessRepoName(repo, pkg),
pkgIsAbsolute,
validateAndProcessPackageName(pkg, target),
validateAndProcessTargetName(pkg, target),
raw);
}

/**
* Parses a raw label string into parts. The logic can be summarized by the following table:
*
* {@code
* raw | repo | pkgIsAbsolute | pkg | target
* ---------------------+--------+---------------+-----------+-----------
* foo/bar | null | false | "" | "foo/bar"
* //foo/bar | null | true | "foo/bar" | "bar"
* @repo | "repo" | true | "" | "repo"
* @repo//foo/bar | "repo" | true | "foo/bar" | "bar"
* :quux | null | false | "" | "quux"
* foo/bar:quux | null | false | "foo/bar" | "quux"
* //foo/bar:quux | null | true | "foo/bar" | "quux"
* @repo//foo/bar:quux | "repo" | true | "foo/bar" | "quux"
* }
*/
static Parts parse(String rawLabel) throws LabelSyntaxException {
@Nullable final String repo;
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(1);
return validateAndCreate(
repo, /*pkgIsAbsolute=*/ true, /*pkg=*/ "", /*target=*/ repo, rawLabel);
} else {
repo = rawLabel.substring(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);
if (colonIndex >= 0) {
pkg = rawLabel.substring(startOfPackage, colonIndex);
target = rawLabel.substring(colonIndex + 1);
} else if (pkgIsAbsolute) {
// Special case: the label "[@repo]//foo/bar" is synonymous with "[@repo]//foo/bar:bar".
pkg = rawLabel.substring(startOfPackage);
// The target name is the last package segment (works even if `pkg` contains no slash)
target = pkg.substring(pkg.lastIndexOf('/') + 1);
} else {
// Special case: the label "foo/bar" is synonymous with ":foo/bar".
pkg = "";
target = rawLabel.substring(startOfPackage);
}
return validateAndCreate(repo, pkgIsAbsolute, pkg, target, rawLabel);
}

@Nullable
private static String validateAndProcessRepoName(@Nullable String repo, String pkg)
throws LabelSyntaxException {
if (repo == null && ABSOLUTE_PACKAGE_NAMES.contains(pkg)) {
// These package names when used without a "@" part are always absolutely in the main repo.
return "";
}
if (repo == null) {
return null;
}
String error = RepositoryName.validate('@' + repo);
if (error != null) {
throw syntaxErrorf("invalid repository name '@%s': %s", repo, error);
}
return repo;
}

private static String validateAndProcessPackageName(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));
}
return pkg;
}

void checkPkgIsAbsolute() throws LabelSyntaxException {
if (!pkgIsAbsolute) {
throw syntaxErrorf("invalid label '%s': absolute label must begin with '@' or '//'", 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)
throws LabelSyntaxException {
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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,8 @@
import javax.annotation.concurrent.Immutable;

/**
* Uniquely identifies a package, given a repository name and a package's path fragment.
*
* <p>The repository the build is happening in is the <i>default workspace</i>, and is identified by
* the workspace name "". Other repositories can be named in the WORKSPACE file. These workspaces
* are prefixed by {@literal @}.
* Uniquely identifies a package. Contains the (canonical) name of the repository this package lives
* in, and the package's path fragment.
*/
@AutoCodec
@Immutable
Expand Down Expand Up @@ -114,44 +111,15 @@ private PackageIdentifier(RepositoryName repository, PathFragment pkgName) {
}

public static PackageIdentifier parse(String input) throws LabelSyntaxException {
return parse(input, /* repo= */ null, /* repositoryMapping= */ null);
}

public static PackageIdentifier parse(
String input, String repo, RepositoryMapping repositoryMapping) throws LabelSyntaxException {
String packageName;
int packageStartPos = input.indexOf("//");
if (repo != null) {
packageName = input;
} else if (input.startsWith("@") && packageStartPos > 0) {
repo = input.substring(0, packageStartPos);
packageName = input.substring(packageStartPos + 2);
} else if (input.startsWith("@")) {
throw new LabelSyntaxException("starts with a '@' but does not contain '//'");
} else if (packageStartPos == 0) {
repo = RepositoryName.DEFAULT_REPOSITORY;
packageName = input.substring(2);
} else {
repo = RepositoryName.DEFAULT_REPOSITORY;
packageName = input;
}

String error = RepositoryName.validate(repo);
if (error != null) {
throw new LabelSyntaxException(error);
}

error = LabelValidator.validatePackageName(packageName);
if (error != null) {
throw new LabelSyntaxException(error);
}

if (repositoryMapping == null) {
return create(repo, PathFragment.create(packageName));
if (input.contains(":")) {
throw LabelParser.syntaxErrorf("invalid package identifier '%s': contains ':'", input);
}

return create(
repositoryMapping.get(RepositoryName.create(repo)), PathFragment.create(packageName));
LabelParser.Parts parts = LabelParser.Parts.parse(input + ":dummy_target");
RepositoryName repoName =
parts.repo == null
? RepositoryName.MAIN
: RepositoryName.createFromValidStrippedName(parts.repo);
return create(repoName, PathFragment.create(parts.pkg));
}

public RepositoryName getRepository() {
Expand All @@ -175,8 +143,9 @@ public PathFragment getSourceRoot() {
* this is in the main repository or siblingRepositoryLayout is true. Otherwise, returns
* external/[repository name]/[pkgName].
*/
// TODO(bazel-team): Rename getDerivedArtifactPath or similar.
public PathFragment getPackagePath(boolean siblingRepositoryLayout) {
return repository.isDefault() || repository.isMain() || siblingRepositoryLayout
return repository.isMain() || siblingRepositoryLayout
? pkgName
: LabelConstants.EXTERNAL_PATH_PREFIX
.getRelative(repository.strippedName())
Expand All @@ -195,30 +164,28 @@ public PathFragment getRunfilesPath() {
return repository.getRunfilesPath().getRelative(pkgName);
}

public PackageIdentifier makeAbsolute() {
if (!repository.isDefault()) {
return this;
}

return create(RepositoryName.MAIN, pkgName);
}

/** Returns the package in label syntax format. */
/**
* Returns the package in label syntax format.
*
* <p>Packages in the main repo are formatted without a repo qualifier.
*/
// TODO(bazel-team): Maybe rename to "getDefaultForm"?
public String getCanonicalForm() {
String repository = getRepository().getCanonicalForm();
return repository + "//" + getPackageFragment();
}

/**
* Returns the name of this package.
* Returns the package path, possibly qualified with a repository name.
*
* <p>There are certain places that expect the path fragment as the package name ('foo/bar') as a
* package identifier. This isn't specific enough for packages in other repositories, so their
* stringified version is '@baz//foo/bar'.
* <p>Packages that live in the main repo are stringified without a "@" qualifier or "//"
* separator (e.g. "foo/bar"). All other packages include these (e.g. "@repo//foo/bar").
*/
// TODO(bazel-team): The absence of "//" for the main repo seems strange. Can we eliminate
// that disparity?
@Override
public String toString() {
return (repository.isDefault() || repository.isMain() ? "" : repository + "//") + pkgName;
return (repository.isMain() ? "" : repository + "//") + pkgName;
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -79,11 +79,9 @@ public RepositoryMapping withAdditionalMappings(RepositoryMapping additionalMapp
}

public RepositoryName get(RepositoryName repositoryName) {
// 1. Default repo ("") should always map to default repo
// 2. @bazel_tools is a special repo that should be visible to all repositories.
// 3. @local_config_platform is a special repo that should be visible to all repositories.
if (repositoryName.equals(RepositoryName.DEFAULT)
|| repositoryName.equals(RepositoryName.BAZEL_TOOLS)
// 1. @bazel_tools is a special repo that should be visible to all repositories.
// 2. @local_config_platform is a special repo that should be visible to all repositories.
if (repositoryName.equals(RepositoryName.BAZEL_TOOLS)
|| repositoryName.equals(RepositoryName.LOCAL_CONFIG_PLATFORM)) {
return repositoryName;
}
Expand Down
Loading

0 comments on commit 103ea9d

Please sign in to comment.