blob: e520756bf77b1c095930051bde3ac6a768f24814 [file] [log] [blame]
use std::iter;
use std::vec;
use aho_corasick::AhoCorasick;
use lazy_static::lazy_static;
use proc_macro2::{Span, TokenStream};
use quote::quote_spanned;
use syn::parse::{Parse, ParseStream};
use syn::{Error, Ident, Lit, LitStr, Result, Token};
/// The possible renaming modes for this macro.
pub enum Mode {
/// No renaming will be done; the expansion will replace each label with
/// just the target.
NoRenaming,
/// First-party crates will be renamed, and third-party crates will not be.
/// The expansion will replace first-party labels with an encoded version,
/// and third-party labels with just their target.
RenameFirstPartyCrates { third_party_dir: String },
}
/// A special case of label::Label, which must be absolute and must not specify
/// a repository.
#[derive(Debug, PartialEq)]
struct AbsoluteLabel<'s> {
package_name: &'s str,
name: &'s str,
}
impl<'s> AbsoluteLabel<'s> {
/// Parses a string as an absolute Bazel label. Labels must be for the
/// current repository.
fn parse(label: &'s str, span: &'s Span) -> Result<Self> {
if let Ok(label::Label {
repository_name: None,
package_name: Some(package_name),
name,
}) = label::analyze(label)
{
Ok(AbsoluteLabel { package_name, name })
} else {
Err(Error::new(
*span,
"Bazel labels must be of the form '//package[:target]'",
))
}
}
/// Returns true iff this label should be renamed.
fn should_rename(&self, mode: &Mode) -> bool {
match mode {
Mode::NoRenaming => false,
Mode::RenameFirstPartyCrates { third_party_dir } => {
!self.package_name.starts_with(third_party_dir)
}
}
}
/// Returns the appropriate (encoded) alias to use, if this label is being
/// renamed; otherwise, returns None.
fn target_as_alias(&self, mode: &Mode) -> Option<String> {
self.should_rename(mode).then(|| encode(self.name))
}
/// Returns the full crate name, encoded if necessary.
fn crate_name(&self, mode: &Mode) -> String {
if self.should_rename(mode) {
encode(&format!("{}:{}", self.package_name, self.name))
} else {
self.name.to_string()
}
}
}
lazy_static! {
// The (unencoded, encoded) pairings here must match those in
// //rust/private/utils.bzl exactly.
static ref ENCODINGS: Vec<(&'static str, &'static str)> =
vec![
(":", "c"),
("!", "bang"),
("%", "perc"),
("@", "at"),
("^", "caret"),
("`", "bt"),
(" ", "sp"),
("\"", "dq"),
("#", "hash"),
("$", "dollar"),
("&", "amp"),
("'", "sq"),
("(", "lp"),
(")", "rp"),
("*", "star"),
("-", "d"),
("+", "plus"),
(",", "comma"),
(";", "semi"),
("<", "langle"),
("=", "eq"),
(">", "rangle"),
("?", "qm"),
("[", "lbrack"),
("]", "rbrack"),
("{", "lbrace"),
("|", "pipe"),
("}", "rbrace"),
("~", "tilde"),
("/", "s"),
(".", "dot"),
];
// Transformations are stored as "(unencoded, encoded)" tuples.
// Target names can include:
// !%-@^_` "#$&'()*-+,;<=>?[]{|}~/.
//
// Package names are alphanumeric, plus [_/-].
//
// Packages and targets are separated by colons.
static ref SUBSTITUTIONS: (Vec<String>, Vec<String>) =
iter::once(("_z".to_string(), "_zz_".to_string()))
.chain(ENCODINGS.iter()
.flat_map(|pair| {
vec![
(format!("_{}_", &pair.1), format!("_z{}_", &pair.1)),
(pair.0.to_string(), format!("_{}_", &pair.1)),
].into_iter()
})
)
.unzip();
static ref ENCODER: AhoCorasick = AhoCorasick::new(&SUBSTITUTIONS.0);
static ref DECODER: AhoCorasick = AhoCorasick::new(&SUBSTITUTIONS.1);
}
/// Encodes a string using the above encoding scheme.
fn encode(s: &str) -> String {
ENCODER.replace_all(s, &SUBSTITUTIONS.1)
}
struct Import {
label: LitStr,
alias: Option<Ident>,
}
impl Import {
fn try_into_statement(self, mode: &Mode) -> Result<proc_macro2::TokenStream> {
let label_literal = &self.label.value();
let span = self.label.span();
let label = AbsoluteLabel::parse(label_literal, &span)?;
let crate_name = &label.crate_name(mode);
let crate_ident = Ident::new(crate_name, span);
let alias = self
.alias
.or_else(|| {
label
.target_as_alias(mode)
.map(|alias| Ident::new(&alias, span))
})
.filter(|alias| alias != crate_name);
Ok(if let Some(alias) = alias {
quote_spanned! {span=> extern crate #crate_ident as #alias; }
} else {
quote_spanned! {span=> extern crate #crate_ident;}
})
}
}
pub struct ImportMacroInput {
imports: Vec<Import>,
}
impl Parse for ImportMacroInput {
fn parse(input: ParseStream) -> Result<Self> {
let mut imports: Vec<Import> = Vec::new();
while !input.is_empty() {
let label = match Lit::parse(input)
.map_err(|_| input.error("expected Bazel label as a string literal"))?
{
Lit::Str(label) => label,
lit => {
return Err(input.error(format!(
"expected Bazel label as string literal, found '{}' literal",
quote::quote! {#lit}
)));
}
};
let alias = if input.peek(Token![as]) {
<Token![as]>::parse(input)?;
Some(
Ident::parse(input)
.map_err(|_| input.error("alias must be a valid Rust identifier"))?,
)
} else {
None
};
imports.push(Import { label, alias });
<syn::Token![;]>::parse(input)?;
}
Ok(Self { imports })
}
}
pub fn expand_imports(
input: ImportMacroInput,
mode: &Mode,
) -> std::result::Result<TokenStream, Vec<syn::Error>> {
let (statements, errs): (Vec<_>, Vec<_>) = input
.imports
.into_iter()
.map(|i| i.try_into_statement(mode))
.partition(Result::is_ok);
if !errs.is_empty() {
Err(errs.into_iter().map(Result::unwrap_err).collect())
} else {
Ok(statements.into_iter().map(Result::unwrap).collect())
}
}
#[cfg(test)]
mod tests {
use crate::*;
use quickcheck::quickcheck;
use syn::parse_quote;
/// Decodes a string that was encoded using `encode`.
fn decode(s: &str) -> String {
DECODER.replace_all(s, &SUBSTITUTIONS.0)
}
#[test]
fn test_expand_imports_without_renaming() -> std::result::Result<(), Vec<syn::Error>> {
let mode = Mode::NoRenaming;
// Nothing to do.
let expanded = expand_imports(parse_quote! {}, &mode)?;
assert_eq!(expanded.to_string(), "");
// Package and a target.
let expanded = expand_imports(parse_quote! { "//some_project:utils"; }, &mode)?;
assert_eq!(expanded.to_string(), "extern crate utils ;");
// Package and a target, with a no-op alias.
let expanded = expand_imports(parse_quote! { "//some_project:utils"; }, &mode)?;
assert_eq!(expanded.to_string(), "extern crate utils ;");
// Package and a target, with an alias.
let expanded = expand_imports(parse_quote! { "//some_project:utils" as my_utils; }, &mode)?;
assert_eq!(expanded.to_string(), "extern crate utils as my_utils ;");
// Package and an implicit target.
let expanded = expand_imports(parse_quote! { "//some_project/utils"; }, &mode)?;
assert_eq!(expanded.to_string(), "extern crate utils ;");
// Package and an implicit target, with a no-op alias.
let expanded = expand_imports(parse_quote! { "//some_project/utils" as utils; }, &mode)?;
assert_eq!(expanded.to_string(), "extern crate utils ;");
// Package and an implicit target, with an alias.
let expanded = expand_imports(parse_quote! { "//some_project:utils" as my_utils; }, &mode)?;
assert_eq!(expanded.to_string(), "extern crate utils as my_utils ;");
// A third-party target.
let expanded =
expand_imports(parse_quote! { "//third_party/rust/serde/v1:serde"; }, &mode)?;
assert_eq!(expanded.to_string(), "extern crate serde ;");
// A third-party target with a no-op alias.
let expanded = expand_imports(
parse_quote! { "//third_party/rust/serde/v1:serde" as serde; },
&mode,
)?;
assert_eq!(expanded.to_string(), "extern crate serde ;");
// A third-party target with an alias.
let expanded = expand_imports(
parse_quote! { "//third_party/rust/serde/v1:serde" as my_serde; },
&mode,
)?;
assert_eq!(expanded.to_string(), "extern crate serde as my_serde ;");
// Multiple targets.
let expanded = expand_imports(
parse_quote! { "//some_project:utils"; "//third_party/rust/serde/v1:serde"; },
&mode,
)?;
assert_eq!(
expanded.to_string(),
"extern crate utils ; extern crate serde ;"
);
Ok(())
}
#[test]
fn test_expand_imports_with_renaming() -> std::result::Result<(), Vec<syn::Error>> {
let mode = Mode::RenameFirstPartyCrates {
third_party_dir: "third_party/rust".to_string(),
};
// Nothing to do.
let expanded = expand_imports(parse_quote! {}, &mode)?;
assert_eq!(expanded.to_string(), "");
// Package and a target.
let expanded = expand_imports(parse_quote! { "//some_project:utils"; }, &mode)?;
assert_eq!(
expanded.to_string(),
"extern crate some_project_c_utils as utils ;"
);
// Package and a target, with a no-op alias.
let expanded = expand_imports(parse_quote! { "//some_project:utils" as utils; }, &mode)?;
assert_eq!(
expanded.to_string(),
"extern crate some_project_c_utils as utils ;"
);
// Package and a target, with an alias.
let expanded = expand_imports(parse_quote! { "//some_project:utils" as my_utils; }, &mode)?;
assert_eq!(
expanded.to_string(),
"extern crate some_project_c_utils as my_utils ;"
);
// Package and an implicit target.
let expanded = expand_imports(parse_quote! { "//some_project/utils"; }, &mode)?;
assert_eq!(
expanded.to_string(),
"extern crate some_project_s_utils_c_utils as utils ;"
);
// Package and an implicit target, with a no-op alias.
let expanded = expand_imports(parse_quote! { "//some_project/utils" as utils; }, &mode)?;
assert_eq!(
expanded.to_string(),
"extern crate some_project_s_utils_c_utils as utils ;"
);
// Package and an implicit target, with an alias.
let expanded = expand_imports(parse_quote! { "//some_project/utils" as my_utils; }, &mode)?;
assert_eq!(
expanded.to_string(),
"extern crate some_project_s_utils_c_utils as my_utils ;"
);
// A third-party target.
let expanded =
expand_imports(parse_quote! { "//third_party/rust/serde/v1:serde"; }, &mode)?;
assert_eq!(expanded.to_string(), "extern crate serde ;");
// A third-party target with a no-op alias.
let expanded = expand_imports(
parse_quote! { "//third_party/rust/serde/v1:serde" as serde; },
&mode,
)?;
assert_eq!(expanded.to_string(), "extern crate serde ;");
// A third-party target with an alias.
let expanded = expand_imports(
parse_quote! { "//third_party/rust/serde/v1:serde" as my_serde; },
&mode,
)?;
assert_eq!(expanded.to_string(), "extern crate serde as my_serde ;");
// Multiple targets.
let expanded = expand_imports(
parse_quote! { "//some_project:utils"; "//third_party/rust/serde/v1:serde"; },
&mode,
)?;
assert_eq!(
expanded.to_string(),
"extern crate some_project_c_utils as utils ; extern crate serde ;"
);
// Problematic target name.
let expanded = expand_imports(parse_quote! { "//some_project:thing-types"; }, &mode)?;
assert_eq!(
expanded.to_string(),
"extern crate some_project_c_thing_d_types as thing_d_types ;"
);
// Problematic target name with alias.
let expanded = expand_imports(
parse_quote! { "//some_project:thing-types" as types; },
&mode,
)?;
assert_eq!(
expanded.to_string(),
"extern crate some_project_c_thing_d_types as types ;"
);
// Problematic package name.
let expanded = expand_imports(parse_quote! { "//some_project-prototype:utils"; }, &mode)?;
assert_eq!(
expanded.to_string(),
"extern crate some_project_d_prototype_c_utils as utils ;"
);
// Problematic package and target names.
let expanded = expand_imports(
parse_quote! { "//some_project-prototype:thing-types"; },
&mode,
)?;
assert_eq!(
expanded.to_string(),
"extern crate some_project_d_prototype_c_thing_d_types as thing_d_types ;"
);
Ok(())
}
#[test]
fn test_expansion_failures() -> Result<()> {
let mode = Mode::NoRenaming;
// Missing leading "//", not a valid label.
let errs = expand_imports(parse_quote! { "some_project:utils"; }, &mode).unwrap_err();
assert_eq!(
errs.into_iter()
.map(|e| e.to_string())
.collect::<Vec<String>>(),
vec!["Bazel labels must be of the form '//package[:target]'"]
);
// Valid label, but relative.
let errs = expand_imports(parse_quote! { ":utils"; }, &mode).unwrap_err();
assert_eq!(
errs.into_iter()
.map(|e| e.to_string())
.collect::<Vec<String>>(),
vec!["Bazel labels must be of the form '//package[:target]'"]
);
// Valid label, but a wildcard.
let errs = expand_imports(parse_quote! { "some_project/..."; }, &mode).unwrap_err();
assert_eq!(
errs.into_iter()
.map(|e| e.to_string())
.collect::<Vec<String>>(),
vec!["Bazel labels must be of the form '//package[:target]'"]
);
// Valid label, but only in Bazel (not in Bazel).
let errs =
expand_imports(parse_quote! { "@repository//some_project:utils"; }, &mode).unwrap_err();
assert_eq!(
errs.into_iter()
.map(|e| e.to_string())
.collect::<Vec<String>>(),
vec!["Bazel labels must be of the form '//package[:target]'"]
);
Ok(())
}
#[test]
fn test_macro_input_parsing_errors() -> Result<()> {
// Label is not a string literal.
assert_eq!(
syn::parse_str::<ImportMacroInput>("some_project:utils;")
.err()
.unwrap()
.to_string(),
"expected Bazel label as a string literal"
);
// Label is the wrong kind of literal.
assert_eq!(
syn::parse_str::<ImportMacroInput>("true;")
.err()
.unwrap()
.to_string(),
"expected Bazel label as string literal, found 'true' literal"
);
assert_eq!(
syn::parse_str::<ImportMacroInput>("123;")
.err()
.unwrap()
.to_string(),
"expected Bazel label as string literal, found '123' literal"
);
// Alias is not a valid identifier.
assert_eq!(
syn::parse_str::<ImportMacroInput>(r#""some_project:utils" as "!@#$%";"#)
.err()
.unwrap()
.to_string(),
"alias must be a valid Rust identifier"
);
Ok(())
}
#[test]
fn test_label_parsing() -> Result<()> {
assert_eq!(
AbsoluteLabel::parse("//some_project:utils", &Span::call_site())?,
AbsoluteLabel {
package_name: "some_project",
name: "utils"
},
);
assert_eq!(
AbsoluteLabel::parse("//some_project/utils", &Span::call_site())?,
AbsoluteLabel {
package_name: "some_project/utils",
name: "utils"
},
);
assert_eq!(
AbsoluteLabel::parse("//some_project", &Span::call_site())?,
AbsoluteLabel {
package_name: "some_project",
name: "some_project"
},
);
Ok(())
}
#[test]
fn test_substitutions_concatenate() -> Result<()> {
// Every combination of orig + orig, orig + encoded, encoded + orig, and
// encoded + encoded round trips the encoding successfully.
for s in SUBSTITUTIONS.0.iter().chain(SUBSTITUTIONS.1.iter()) {
for t in SUBSTITUTIONS.0.iter().chain(SUBSTITUTIONS.1.iter()) {
let concatenated = format!("{}{}", s, t);
assert_eq!(&decode(&encode(&concatenated)), &concatenated);
}
}
Ok(())
}
#[test]
fn test_encode() -> Result<()> {
assert_eq!(encode("some_project:utils"), "some_project_c_utils");
assert_eq!(&encode("_zdot_"), "_zz_dot_");
// All the encodings should be what we expect.
for (orig, encoded) in SUBSTITUTIONS.0.iter().zip(SUBSTITUTIONS.1.iter()) {
assert_eq!(&encode(orig), encoded);
}
Ok(())
}
#[test]
fn test_decode() -> Result<()> {
assert_eq!(decode("some_project_c_utils"), "some_project:utils");
assert_eq!(decode("_zz_dot_"), "_zdot_");
// All the decodings should be what we expect.
for (orig, encoded) in SUBSTITUTIONS.0.iter().zip(SUBSTITUTIONS.1.iter()) {
assert_eq!(&decode(encoded), orig);
}
Ok(())
}
#[test]
fn test_substitutions_compose() -> Result<()> {
for s in SUBSTITUTIONS.0.iter().chain(SUBSTITUTIONS.1.iter()) {
assert_eq!(&decode(&encode(s)), s);
}
Ok(())
}
quickcheck! {
fn composition_is_identity(s: String) -> bool {
s == decode(&encode(&s))
}
}
}