blob: bff99842626d11d4c5d1ba6868a9ce59bbb18143 [file] [log] [blame]
//! Bazel label parsing library.
//!
//! USAGE: `label::analyze("//foo/bar:baz")
mod label_error;
use label_error::LabelError;
/// Parse and analyze given str.
///
/// 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.",
)))
}
(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,
}
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,
}
}
pub fn packages(&self) -> Vec<&'s str> {
match self.package_name {
Some(name) => name.split('/').collect(),
None => vec![],
}
}
}
fn err<'s>(label: &'s str, msg: &'s str) -> String {
let mut err_msg = label.to_string();
err_msg.push_str(" must be a legal label; ");
err_msg.push_str(msg);
err_msg
}
fn consume_repository_name<'s>(
input: &'s str,
label: &'s str,
) -> Result<(&'s str, Option<&'s str>)> {
if !input.starts_with('@') {
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 !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 '.'.",
)));
}
Ok((&input[slash_pos..], Some(repository_name)))
}
fn consume_package_name<'s>(input: &'s str, label: &'s str) -> Result<(&'s str, Option<&'s str>)> {
let is_absolute = match input.rfind("//") {
None => false,
Some(0) => true,
Some(_) => {
return Err(LabelError(err(
label,
"'//' cannot appear in the middle of the label.",
)));
}
};
let (package_name, rest) = match (is_absolute, input.find(':')) {
(false, colon_pos) if colon_pos.map_or(true, |pos| pos != 0) => {
return Err(LabelError(err(
label,
"relative packages are not permitted.",
)));
}
(_, colon_pos) => {
let (input, colon_pos) = if is_absolute {
(&input[2..], colon_pos.map(|cp| cp - 2))
} else {
(input, colon_pos)
};
match colon_pos {
Some(colon_pos) => (&input[0..colon_pos], &input[colon_pos..]),
None => (input, ""),
}
}
};
if package_name.is_empty() {
return Ok((rest, None));
}
if !package_name.chars().all(|c| {
c.is_ascii_alphanumeric()
|| c == '/'
|| c == '-'
|| c == '.'
|| c == ' '
|| c == '$'
|| c == '('
|| c == ')'
|| c == '_'
}) {
return Err(LabelError(err(
label,
"package names may contain only A-Z, \
a-z, 0-9, '/', '-', '.', ' ', '$', '(', ')' and '_'.",
)));
}
if package_name.ends_with('/') {
return Err(LabelError(err(
label,
"package names may not end with '/'.",
)));
}
if rest.is_empty() && is_absolute {
// This label doesn't contain the target name, we have to use
// last segment of the package name as target name.
return Ok((
match package_name.rfind('/') {
Some(pos) => &package_name[pos..],
None => package_name,
},
Some(package_name),
));
}
Ok((rest, Some(package_name)))
}
fn consume_name<'s>(input: &'s str, label: &'s str) -> Result<Option<&'s str>> {
if input.is_empty() {
return Ok(None);
}
if input == ":" {
return Err(LabelError(err(label, "empty target name.")));
}
let name = input
.strip_prefix(':')
.or_else(|| input.strip_prefix('/'))
.unwrap_or(input);
if name.starts_with('/') {
return Err(LabelError(err(
label,
"target names may not start with '/'.",
)));
}
Ok(Some(name))
}
fn name_from_package(package_name: &str) -> &str {
package_name
.rsplit_once('/')
.map(|tup| tup.1)
.unwrap_or(package_name)
}
#[cfg(test)]
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("foo/bar"),
Err(LabelError(
"foo/bar must be a legal label; relative packages are not permitted.".to_string()
))
);
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"),
Err(LabelError(
"foo must be a legal label; relative packages are not permitted.".to_string()
))
);
assert_eq!(
analyze("@foo:bar"),
Err(LabelError(
"@foo:bar must be a legal label; labels with repository must contain //."
.to_string()
))
);
assert_eq!(
analyze("@AZab0123456789_-.//:foo")?.repository_name,
Some("AZab0123456789_-.")
);
assert_eq!(
analyze("@42//:baz"),
Err(LabelError(
"@42//:baz must be a legal label; workspace names must \
start with a letter."
.to_string()
))
);
assert_eq!(
analyze("@foo#//:baz"),
Err(LabelError(
"@foo#//:baz must be a legal label; workspace names \
may contain only A-Z, a-z, 0-9, '-', '_', and '.'."
.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("//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"),
Err(LabelError(
"foo:baz/qux must be a legal label; relative packages are not permitted."
.to_string()
))
);
assert_eq!(
analyze("foo/bar:baz/qux"),
Err(LabelError(
"foo/bar:baz/qux must be a legal label; relative packages are not permitted."
.to_string()
))
);
assert_eq!(analyze("//foo")?.package_name, Some("foo"));
assert_eq!(
analyze("foo//bar"),
Err(LabelError(
"foo//bar must be a legal label; '//' cannot appear in the middle of the label."
.to_string()
))
);
assert_eq!(
analyze("//foo//bar"),
Err(LabelError(
"//foo//bar must be a legal label; '//' cannot appear in the middle of the label."
.to_string()
))
);
assert_eq!(
analyze("foo//bar:baz"),
Err(LabelError(
"foo//bar:baz must be a legal label; '//' cannot appear in the middle of the label."
.to_string()
))
);
assert_eq!(
analyze("//foo//bar:baz"),
Err(LabelError(
"//foo//bar:baz must be a legal label; '//' cannot appear in the middle of the label."
.to_string()
))
);
assert_eq!(
analyze("//azAZ09/-. $()_:baz")?.package_name,
Some("azAZ09/-. $()_")
);
assert_eq!(
analyze("//bar#:baz"),
Err(LabelError(
"//bar#:baz must be a legal label; package names may contain only A-Z, \
a-z, 0-9, '/', '-', '.', ' ', '$', '(', ')' and '_'."
.to_string()
))
);
assert_eq!(
analyze("//bar/:baz"),
Err(LabelError(
"//bar/:baz must be a legal label; package names may not end with '/'.".to_string()
))
);
assert_eq!(analyze("@repo//foo/bar")?.package_name, Some("foo/bar"));
assert_eq!(analyze("//foo/bar")?.package_name, Some("foo/bar"));
assert_eq!(
analyze("foo/bar"),
Err(LabelError(
"foo/bar must be a legal label; relative packages are not permitted.".to_string()
))
);
assert_eq!(analyze("@repo//foo")?.package_name, Some("foo"));
assert_eq!(analyze("//foo")?.package_name, Some("foo"));
assert_eq!(
analyze("foo"),
Err(LabelError(
"foo must be a legal label; relative packages are not permitted.".to_string()
))
);
Ok(())
}
#[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("//bar:"),
Err(LabelError(
"//bar: must be a legal label; empty target name.".to_string()
))
);
assert_eq!(analyze("//foo")?.name, "foo");
assert_eq!(
analyze("//bar:/foo"),
Err(LabelError(
"//bar:/foo must be a legal label; target names may not start with '/'."
.to_string()
))
);
assert_eq!(analyze("@repo//foo/bar")?.name, "bar");
assert_eq!(analyze("//foo/bar")?.name, "bar");
assert_eq!(
analyze("foo/bar"),
Err(LabelError(
"foo/bar must be a legal label; relative packages are not permitted.".to_string()
))
);
assert_eq!(analyze("@repo//foo")?.name, "foo");
assert_eq!(analyze("//foo")?.name, "foo");
assert_eq!(
analyze("foo"),
Err(LabelError(
"foo must be a legal label; relative packages are not permitted.".to_string()
))
);
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"]
);
Ok(())
}
}