From dbdef6eac5349afbfdf9923911fd9720160e02f7 Mon Sep 17 00:00:00 2001 From: qtica Date: Thu, 22 Feb 2024 06:54:52 -0800 Subject: [PATCH] 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). --- util/label/label.rs | 405 +++++++++++++++++++++++++++++++------------- 1 file changed, 284 insertions(+), 121 deletions(-) diff --git a/util/label/label.rs b/util/label/label.rs index fc6469d2f6..d1e00a7ec3 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> { - 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>, + package_name: &'s str, + target_name: &'s str, + }, } type Result = core::result::Result; 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> { + 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>)> { + 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> { 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)) } @@ -209,28 +331,18 @@ fn name_from_package(package_name: &str) -> &str { 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( @@ -248,16 +361,29 @@ 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(())