aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorUebelAndre <github@uebelandre.com>2024-02-22 06:54:52 -0800
committerGitHub <noreply@github.com>2024-02-22 14:54:52 +0000
commit998f7d532af56370f726ed6718c0dff13d39abd7 (patch)
tree0742b7c9d4cc2637ede5a654dc82b608fdfcf348
parente404e51639637c75e246a290823d978e20f70b63 (diff)
downloadbazelbuild-rules_rust-998f7d532af56370f726ed6718c0dff13d39abd7.tar.gz
Updated label crate to understand canonical vs apparent repositories (#2507)
Doing my best to adhere to https://bazel.build/concepts/labels. This change updates the label utility to account for modern changes to Bazel labels (e.g. from bzlmod).
-rw-r--r--util/label/label.rs405
1 files changed, 284 insertions, 121 deletions
diff --git a/util/label/label.rs b/util/label/label.rs
index fc6469d2..d1e00a7e 100644
--- a/util/label/label.rs
+++ b/util/label/label.rs
@@ -9,49 +9,134 @@ use label_error::LabelError;
/// TODO: validate . and .. in target name
/// TODO: validate used characters in target name
pub fn analyze(input: &'_ str) -> Result<Label<'_>> {
- let label = input;
- let (input, repository_name) = consume_repository_name(input, label)?;
- let (input, package_name) = consume_package_name(input, label)?;
- let name = consume_name(input, label)?;
- let name = match (package_name, name) {
- (None, None) => {
- return Err(LabelError(err(
- label,
- "labels must have a package and/or a name.",
- )))
+ Label::analyze(input)
+}
+
+#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone)]
+pub enum Repository<'s> {
+ /// A `@@` prefixed name that is unique within a workspace. E.g. `@@rules_rust~0.1.2~toolchains~local_rustc`
+ Canonical(&'s str), // stringifies to `@@self.0` where `self.0` may be empty
+ /// A `@` (single) prefixed name. E.g. `@rules_rust`.
+ Apparent(&'s str),
+}
+
+impl<'s> Repository<'s> {
+ pub fn repo_name(&self) -> &'s str {
+ match self {
+ Repository::Canonical(name) => &name[2..],
+ Repository::Apparent(name) => &name[1..],
}
- (Some(package_name), None) => name_from_package(package_name),
- (_, Some(name)) => name,
- };
- Ok(Label::new(repository_name, package_name, name))
+ }
}
#[derive(Debug, PartialEq, Eq)]
-pub struct Label<'s> {
- pub repository_name: Option<&'s str>,
- pub package_name: Option<&'s str>,
- pub name: &'s str,
+pub enum Label<'s> {
+ Relative {
+ target_name: &'s str,
+ },
+ Absolute {
+ repository: Option<Repository<'s>>,
+ package_name: &'s str,
+ target_name: &'s str,
+ },
}
type Result<T, E = LabelError> = core::result::Result<T, E>;
impl<'s> Label<'s> {
- fn new(
- repository_name: Option<&'s str>,
- package_name: Option<&'s str>,
- name: &'s str,
- ) -> Label<'s> {
- Label {
- repository_name,
- package_name,
- name,
+ /// Parse and analyze given str.
+ pub fn analyze(input: &'s str) -> Result<Label<'s>> {
+ let label = input;
+
+ if label.is_empty() {
+ return Err(LabelError(err(
+ label,
+ "Empty string cannot be parsed into a label.",
+ )));
+ }
+
+ if label.starts_with(':') {
+ return match consume_name(input, label)? {
+ None => Err(LabelError(err(
+ label,
+ "Relative packages must have a name.",
+ ))),
+ Some(name) => Ok(Label::Relative { target_name: name }),
+ };
+ }
+
+ let (input, repository) = consume_repository_name(input, label)?;
+
+ // Shorthand labels such as `@repo` are expanded to `@repo//:repo`.
+ if input.is_empty() {
+ if let Some(ref repo) = repository {
+ let target_name = repo.repo_name();
+ if target_name.is_empty() {
+ return Err(LabelError(err(
+ label,
+ "invalid target name: empty target name",
+ )));
+ } else {
+ return Ok(Label::Absolute {
+ repository,
+ package_name: "",
+ target_name,
+ });
+ };
+ }
+ }
+ let (input, package_name) = consume_package_name(input, label)?;
+ let name = consume_name(input, label)?;
+ let name = match (package_name, name) {
+ (None, None) => {
+ return Err(LabelError(err(
+ label,
+ "labels must have a package and/or a name.",
+ )))
+ }
+ (Some(package_name), None) => name_from_package(package_name),
+ (_, Some(name)) => name,
+ };
+
+ Ok(Label::Absolute {
+ repository,
+ package_name: package_name.unwrap_or_default(),
+ target_name: name,
+ })
+ }
+
+ pub fn is_relative(&self) -> bool {
+ match self {
+ Label::Absolute { .. } => false,
+ Label::Relative { .. } => true,
+ }
+ }
+
+ pub fn repo(&self) -> Option<&Repository<'s>> {
+ match self {
+ Label::Absolute { repository, .. } => repository.as_ref(),
+ Label::Relative { .. } => None,
+ }
+ }
+
+ pub fn repo_name(&self) -> Option<&'s str> {
+ match self {
+ Label::Absolute { repository, .. } => repository.as_ref().map(|repo| repo.repo_name()),
+ Label::Relative { .. } => None,
+ }
+ }
+
+ pub fn package(&self) -> Option<&'s str> {
+ match self {
+ Label::Relative { .. } => None,
+ Label::Absolute { package_name, .. } => Some(*package_name),
}
}
- pub fn packages(&self) -> Vec<&'s str> {
- match self.package_name {
- Some(name) => name.split('/').collect(),
- None => vec![],
+ pub fn name(&self) -> &'s str {
+ match self {
+ Label::Relative { target_name } => target_name,
+ Label::Absolute { target_name, .. } => target_name,
}
}
}
@@ -66,40 +151,67 @@ fn err<'s>(label: &'s str, msg: &'s str) -> String {
fn consume_repository_name<'s>(
input: &'s str,
label: &'s str,
-) -> Result<(&'s str, Option<&'s str>)> {
- if !input.starts_with('@') {
+) -> Result<(&'s str, Option<Repository<'s>>)> {
+ let at_signs = {
+ let mut count = 0;
+ for char in input.chars() {
+ if char == '@' {
+ count += 1;
+ } else {
+ break;
+ }
+ }
+ count
+ };
+ if at_signs == 0 {
return Ok((input, None));
}
-
- let slash_pos = input
- .find("//")
- .ok_or_else(|| err(label, "labels with repository must contain //."))?;
- let repository_name = &input[1..slash_pos];
- if repository_name.is_empty() {
- return Ok((&input[1..], None));
+ if at_signs > 2 {
+ return Err(LabelError(err(label, "Unexpected number of leading `@`.")));
}
- if !repository_name
- .chars()
- .next()
- .unwrap()
- .is_ascii_alphabetic()
- {
- return Err(LabelError(err(
- label,
- "workspace names must start with a letter.",
- )));
- }
- if !repository_name
- .chars()
- .all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_' || c == '.')
- {
- return Err(LabelError(err(
- label,
- "workspace names \
- may contain only A-Z, a-z, 0-9, '-', '_', and '.'.",
- )));
+
+ let slash_pos = input.find("//").unwrap_or(input.len());
+ let repository_name = &input[at_signs..slash_pos];
+
+ if !repository_name.is_empty() {
+ if !repository_name
+ .chars()
+ .next()
+ .unwrap()
+ .is_ascii_alphabetic()
+ {
+ return Err(LabelError(err(
+ label,
+ "workspace names must start with a letter.",
+ )));
+ }
+ if !repository_name
+ .chars()
+ .all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_' || c == '.' || c == '~')
+ {
+ return Err(LabelError(err(
+ label,
+ "workspace names \
+ may contain only A-Z, a-z, 0-9, '-', '_', '.', and '~'.",
+ )));
+ }
}
- Ok((&input[slash_pos..], Some(repository_name)))
+
+ let repository = if at_signs == 1 {
+ Repository::Apparent(&input[0..slash_pos])
+ } else if at_signs == 2 {
+ if repository_name.is_empty() {
+ return Err(LabelError(err(
+ label,
+ "main repository labels are only represented by a single `@`.",
+ )));
+ }
+ Repository::Canonical(&input[0..slash_pos])
+ } else {
+ return Err(LabelError(err(label, "Unexpected number of leading `@`.")));
+ };
+
+ Ok((&input[slash_pos..], Some(repository)))
}
fn consume_package_name<'s>(input: &'s str, label: &'s str) -> Result<(&'s str, Option<&'s str>)> {
@@ -185,16 +297,26 @@ fn consume_name<'s>(input: &'s str, label: &'s str) -> Result<Option<&'s str>> {
if input == ":" {
return Err(LabelError(err(label, "empty target name.")));
}
- let name = input
- .strip_prefix(':')
- .or_else(|| input.strip_prefix('/'))
- .unwrap_or(input);
+ let name = if let Some(stripped) = input.strip_prefix(':') {
+ stripped
+ } else if let Some(stripped) = input.strip_prefix("//") {
+ stripped
+ } else {
+ input.strip_prefix('/').unwrap_or(input)
+ };
+
if name.starts_with('/') {
return Err(LabelError(err(
label,
"target names may not start with '/'.",
)));
}
+ if name.starts_with(':') {
+ return Err(LabelError(err(
+ label,
+ "target names may not contain with ':'.",
+ )));
+ }
Ok(Some(name))
}
@@ -210,27 +332,17 @@ mod tests {
use super::*;
#[test]
- fn test_new() {
- assert_eq!(
- Label::new(Some("repo"), Some("foo/bar"), "baz"),
- Label {
- repository_name: Some("repo"),
- package_name: Some("foo/bar"),
- name: "baz",
- }
- );
- }
-
- #[test]
fn test_repository_name_parsing() -> Result<()> {
- assert_eq!(analyze("@repo//:foo")?.repository_name, Some("repo"));
- assert_eq!(analyze("@//:foo")?.repository_name, None);
- assert_eq!(analyze("//:foo")?.repository_name, None);
- assert_eq!(analyze(":foo")?.repository_name, None);
-
- assert_eq!(analyze("@repo//foo/bar")?.repository_name, Some("repo"));
- assert_eq!(analyze("@//foo/bar")?.repository_name, None);
- assert_eq!(analyze("//foo/bar")?.repository_name, None);
+ assert_eq!(analyze("@repo//:foo")?.repo_name(), Some("repo"));
+ assert_eq!(analyze("@@repo//:foo")?.repo_name(), Some("repo"));
+ assert_eq!(analyze("@//:foo")?.repo_name(), Some(""));
+ assert_eq!(analyze("//:foo")?.repo_name(), None);
+ assert_eq!(analyze(":foo")?.repo_name(), None);
+
+ assert_eq!(analyze("@repo//foo/bar")?.repo_name(), Some("repo"));
+ assert_eq!(analyze("@@repo//foo/bar")?.repo_name(), Some("repo"));
+ assert_eq!(analyze("@//foo/bar")?.repo_name(), Some(""));
+ assert_eq!(analyze("//foo/bar")?.repo_name(), None);
assert_eq!(
analyze("foo/bar"),
Err(LabelError(
@@ -238,9 +350,10 @@ mod tests {
))
);
- assert_eq!(analyze("@repo//foo")?.repository_name, Some("repo"));
- assert_eq!(analyze("@//foo")?.repository_name, None);
- assert_eq!(analyze("//foo")?.repository_name, None);
+ assert_eq!(analyze("@repo//foo")?.repo_name(), Some("repo"));
+ assert_eq!(analyze("@@repo//foo")?.repo_name(), Some("repo"));
+ assert_eq!(analyze("@//foo")?.repo_name(), Some(""));
+ assert_eq!(analyze("//foo")?.repo_name(), None);
assert_eq!(
analyze("foo"),
Err(LabelError(
@@ -249,15 +362,28 @@ mod tests {
);
assert_eq!(
+ analyze("@@@repo//foo"),
+ Err(LabelError(
+ "@@@repo//foo must be a legal label; Unexpected number of leading `@`.".to_owned()
+ ))
+ );
+
+ assert_eq!(
+ analyze("@@@//foo:bar"),
+ Err(LabelError(
+ "@@@//foo:bar must be a legal label; Unexpected number of leading `@`.".to_owned()
+ ))
+ );
+
+ assert_eq!(
analyze("@foo:bar"),
Err(LabelError(
- "@foo:bar must be a legal label; labels with repository must contain //."
- .to_string()
+ "@foo:bar must be a legal label; workspace names may contain only A-Z, a-z, 0-9, '-', '_', '.', and '~'.".to_string()
))
);
assert_eq!(
- analyze("@AZab0123456789_-.//:foo")?.repository_name,
+ analyze("@AZab0123456789_-.//:foo")?.repo_name(),
Some("AZab0123456789_-.")
);
assert_eq!(
@@ -272,19 +398,49 @@ mod tests {
analyze("@foo#//:baz"),
Err(LabelError(
"@foo#//:baz must be a legal label; workspace names \
- may contain only A-Z, a-z, 0-9, '-', '_', and '.'."
+ may contain only A-Z, a-z, 0-9, '-', '_', '.', and '~'."
+ .to_string()
+ ))
+ );
+ assert_eq!(
+ analyze("@@//foo/bar"),
+ Err(LabelError(
+ "@@//foo/bar must be a legal label; main repository labels are only represented by a single `@`."
+ .to_string()
+ ))
+ );
+ assert_eq!(
+ analyze("@@//:foo"),
+ Err(LabelError(
+ "@@//:foo must be a legal label; main repository labels are only represented by a single `@`."
.to_string()
))
);
+ assert_eq!(
+ analyze("@@//foo"),
+ Err(LabelError(
+ "@@//foo must be a legal label; main repository labels are only represented by a single `@`."
+ .to_string()
+ ))
+ );
+
+ assert_eq!(
+ analyze("@@"),
+ Err(LabelError(
+ "@@ must be a legal label; main repository labels are only represented by a single `@`.".to_string()
+ )),
+ );
+
Ok(())
}
+
#[test]
fn test_package_name_parsing() -> Result<()> {
- assert_eq!(analyze("//:baz/qux")?.package_name, None);
- assert_eq!(analyze(":baz/qux")?.package_name, None);
+ assert_eq!(analyze("//:baz/qux")?.package(), Some(""));
+ assert_eq!(analyze(":baz/qux")?.package(), None);
- assert_eq!(analyze("//foo:baz/qux")?.package_name, Some("foo"));
- assert_eq!(analyze("//foo/bar:baz/qux")?.package_name, Some("foo/bar"));
+ assert_eq!(analyze("//foo:baz/qux")?.package(), Some("foo"));
+ assert_eq!(analyze("//foo/bar:baz/qux")?.package(), Some("foo/bar"));
assert_eq!(
analyze("foo:baz/qux"),
Err(LabelError(
@@ -300,7 +456,7 @@ mod tests {
))
);
- assert_eq!(analyze("//foo")?.package_name, Some("foo"));
+ assert_eq!(analyze("//foo")?.package(), Some("foo"));
assert_eq!(
analyze("foo//bar"),
@@ -332,7 +488,7 @@ mod tests {
);
assert_eq!(
- analyze("//azAZ09/-. $()_:baz")?.package_name,
+ analyze("//azAZ09/-. $()_:baz")?.package(),
Some("azAZ09/-. $()_")
);
assert_eq!(
@@ -350,8 +506,8 @@ mod tests {
))
);
- assert_eq!(analyze("@repo//foo/bar")?.package_name, Some("foo/bar"));
- assert_eq!(analyze("//foo/bar")?.package_name, Some("foo/bar"));
+ assert_eq!(analyze("@repo//foo/bar")?.package(), Some("foo/bar"));
+ assert_eq!(analyze("//foo/bar")?.package(), Some("foo/bar"));
assert_eq!(
analyze("foo/bar"),
Err(LabelError(
@@ -359,8 +515,8 @@ mod tests {
))
);
- assert_eq!(analyze("@repo//foo")?.package_name, Some("foo"));
- assert_eq!(analyze("//foo")?.package_name, Some("foo"));
+ assert_eq!(analyze("@repo//foo")?.package(), Some("foo"));
+ assert_eq!(analyze("//foo")?.package(), Some("foo"));
assert_eq!(
analyze("foo"),
Err(LabelError(
@@ -373,8 +529,17 @@ mod tests {
#[test]
fn test_name_parsing() -> Result<()> {
- assert_eq!(analyze("//foo:baz")?.name, "baz");
- assert_eq!(analyze("//foo:baz/qux")?.name, "baz/qux");
+ assert_eq!(analyze("//foo:baz")?.name(), "baz");
+ assert_eq!(analyze("//foo:baz/qux")?.name(), "baz/qux");
+ assert_eq!(analyze(":baz/qux")?.name(), "baz/qux");
+
+ assert_eq!(
+ analyze("::baz/qux"),
+ Err(LabelError(
+ "::baz/qux must be a legal label; target names may not contain with ':'."
+ .to_string()
+ ))
+ );
assert_eq!(
analyze("//bar:"),
@@ -382,7 +547,7 @@ mod tests {
"//bar: must be a legal label; empty target name.".to_string()
))
);
- assert_eq!(analyze("//foo")?.name, "foo");
+ assert_eq!(analyze("//foo")?.name(), "foo");
assert_eq!(
analyze("//bar:/foo"),
@@ -392,8 +557,8 @@ mod tests {
))
);
- assert_eq!(analyze("@repo//foo/bar")?.name, "bar");
- assert_eq!(analyze("//foo/bar")?.name, "bar");
+ assert_eq!(analyze("@repo//foo/bar")?.name(), "bar");
+ assert_eq!(analyze("//foo/bar")?.name(), "bar");
assert_eq!(
analyze("foo/bar"),
Err(LabelError(
@@ -401,8 +566,8 @@ mod tests {
))
);
- assert_eq!(analyze("@repo//foo")?.name, "foo");
- assert_eq!(analyze("//foo")?.name, "foo");
+ assert_eq!(analyze("@repo//foo")?.name(), "foo");
+ assert_eq!(analyze("//foo")?.name(), "foo");
assert_eq!(
analyze("foo"),
Err(LabelError(
@@ -410,22 +575,20 @@ mod tests {
))
);
- Ok(())
- }
-
- #[test]
- fn test_packages() -> Result<()> {
- assert_eq!(analyze("@repo//:baz")?.packages(), Vec::<&str>::new());
- assert_eq!(analyze("@repo//foo:baz")?.packages(), vec!["foo"]);
assert_eq!(
- analyze("@repo//foo/bar:baz")?.packages(),
- vec!["foo", "bar"]
+ analyze("@repo")?,
+ Label::Absolute {
+ repository: Some(Repository::Apparent("@repo")),
+ package_name: "",
+ target_name: "repo",
+ },
);
- // Plus (+) is valid in packages
assert_eq!(
- analyze("@repo//foo/bar+baz:qaz")?.packages(),
- vec!["foo", "bar+baz"]
+ analyze("@"),
+ Err(LabelError(
+ "@ must be a legal label; invalid target name: empty target name".to_string()
+ )),
);
Ok(())