From d6196637fe0677f0b5807e096c673014b3906817 Mon Sep 17 00:00:00 2001 From: cfredric Date: Wed, 10 Nov 2021 16:33:01 -0500 Subject: [PATCH] Add new `import` macro prototype. --- util/import_macro/BUILD.bazel | 34 ++ util/import_macro/import.rs | 15 + util/import_macro/import_internal.rs | 478 +++++++++++++++++++++++++++ 3 files changed, 527 insertions(+) create mode 100644 util/import_macro/BUILD.bazel create mode 100644 util/import_macro/import.rs create mode 100644 util/import_macro/import_internal.rs diff --git a/util/import_macro/BUILD.bazel b/util/import_macro/BUILD.bazel new file mode 100644 index 0000000000..03d20b18ca --- /dev/null +++ b/util/import_macro/BUILD.bazel @@ -0,0 +1,34 @@ +load("//rust:defs.bzl", "rust_library", "rust_proc_macro", "rust_test") + +rust_proc_macro( + name = "import", + srcs = [ + "import.rs", + ], + deps = [ + ":import_internal", + # syn + ], +) + +rust_library( + name = "import_internal", + srcs = [ + "import_internal.rs", + ], + deps = [ + # aho_corasick + # lazy_static + # proc_macro2 + # quote + # syn + ], +) + +rust_test( + name = "import_internal_test", + crate = ":import_internal", + deps = [ + # quickcheck + ], +) \ No newline at end of file diff --git a/util/import_macro/import.rs b/util/import_macro/import.rs new file mode 100644 index 0000000000..7b3f6e3425 --- /dev/null +++ b/util/import_macro/import.rs @@ -0,0 +1,15 @@ +use proc_macro; +use syn::parse_macro_input; + +use import_internal; + +// Flipping this bool allows easy switching between renaming crates and not. +const RENAME_1P_CRATES: bool = true; + +#[proc_macro] +pub fn import(input: proc_macro::TokenStream) -> proc_macro::TokenStream { + let input = parse_macro_input!(input as import_internal::ImportMacroInput); + import_internal::expand_imports(input, RENAME_1P_CRATES) + .unwrap_or_else(|errors| errors.into_iter().map(|e| e.into_compile_error()).collect()) + .into() +} \ No newline at end of file diff --git a/util/import_macro/import_internal.rs b/util/import_macro/import_internal.rs new file mode 100644 index 0000000000..90514133a5 --- /dev/null +++ b/util/import_macro/import_internal.rs @@ -0,0 +1,478 @@ +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}; + +#[derive(Debug, PartialEq)] +struct BazelLabel { + package: String, + target: String, +} + +// TODO(cfredric): use //rules_rust/util/label/label.rs for this. +impl BazelLabel { + fn parse(label: &str, span: &Span) -> Result { + let (package, target) = label + .rsplit_once(':') + .or_else(|| { + label.rfind('/').map(|last_slash| { + let target = &label[last_slash + 1..]; + (label, target) + }) + }) + .filter(|(package, _target)| package.starts_with("//")) + .ok_or_else(|| { + Error::new(*span, "Bazel labels must be of the form '//package[:target]'") + })?; + let package = &package[2..]; + + Ok(BazelLabel { package: package.to_string(), target: target.to_string() }) + } + + fn is_third_party(&self) -> bool { + self.package.starts_with("third_party/rust/") + } + + fn target(&self) -> String { + mangle(&self.target) + } + + fn crate_name(&self, rename_1p_crate: bool) -> String { + if self.is_third_party() || !rename_1p_crate { + self.target() + } else { + mangle(&format!("{}:{}", self.package, self.target)) + } + } +} + +lazy_static! { + // Transformations are stored as "(unmangled, mangled)" tuples. + // Target names can include: + // !%-@^_` "#$&'()*-+,;<=>?[]{|}~/. + // + // Package names are alphanumeric, plus [_/-]. + // + // Packages and targets are separated by colons. + static ref SUBSTITUTIONS: (Vec, Vec) = + iter::once(("_QUOTE".to_string(), "_QUOTEQUOTE_".to_string())) + .chain( + vec![ + (":", "COLON"), + ("!", "BANG"), + ("%", "PERCENT"), + ("@", "AT"), + ("^", "CARET"), + ("`", "BACKTICK"), + (" ", "SPACE"), + ("\"", "QUOTE"), + ("#", "HASH"), + ("$", "DOLLAR"), + ("&", "AMPERSAND"), + ("'", "BACKSLASH"), + ("(", "LPAREN"), + (")", "RPAREN"), + ("*", "STAR"), + ("-", "DASH"), + ("+", "PLUS"), + (",", "COMMA"), + (";", "SEMICOLON"), + ("<", "LANGLE"), + ("=", "EQUAL"), + (">", "RANGLE"), + ("?", "QUESTION"), + ("[", "LBRACKET"), + ("]", "RBRACKET"), + ("{", "LBRACE"), + ("|", "PIPE"), + ("}", "RBRACE"), + ("~", "TILDE"), + ("/", "SLASH"), + (".", "DOT"), + ].into_iter() + .flat_map(|pair| { + vec![ + (format!("_{}_", &pair.1), format!("_QUOTE{}_", &pair.1)), + (pair.0.to_string(), format!("_{}_", &pair.1)), + ].into_iter() + }) + ) + .unzip(); + + static ref MANGLER: AhoCorasick = AhoCorasick::new(&SUBSTITUTIONS.0); + static ref UNMANGLER: AhoCorasick = AhoCorasick::new(&SUBSTITUTIONS.1); +} + +fn mangle(s: &str) -> String { + MANGLER.replace_all(s, &SUBSTITUTIONS.1) +} + +#[cfg(test)] +fn unmangle(s: &str) -> String { + UNMANGLER.replace_all(s, &SUBSTITUTIONS.0) +} + +#[derive(Debug, PartialEq)] +struct Import { + label: LitStr, + alias: Option, +} + +impl Import { + fn try_into_statement(self, rename_1p_crate: bool) -> Result { + let label = BazelLabel::parse(&self.label.value(), &self.label.span())?; + let crate_name = &label.crate_name(rename_1p_crate); + + let crate_ident = Ident::new(&crate_name, self.label.span()); + + let span = self.label.span(); + Ok( + match self + .alias + .or_else(|| { + if label.is_third_party() { + None + } else { + Some(Ident::new(&label.target(), span)) + } + }) + .filter(|alias: &Ident| alias != crate_name) + { + Some(alias) => { + quote_spanned! {span=> extern crate #crate_ident as #alias; } + } + None => { + quote_spanned! {span=> extern crate #crate_ident;} + } + }, + ) + } +} + +#[derive(Debug, PartialEq)] +pub struct ImportMacroInput { + imports: Vec, +} + +impl Parse for ImportMacroInput { + fn parse(input: ParseStream) -> Result { + let mut imports: Vec = 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 {:?}", + lit + ))); + } + }; + let alias = if input.peek(Token![as]) { + let _as = ::parse(input)?; + Some( + Ident::parse(input) + .map_err(|_e| input.error("alias must be a valid Rust identifier"))?, + ) + } else { + None + }; + imports.push(Import { label, alias }); + let _semicolon = ::parse(input)?; + } + + Ok(ImportMacroInput { imports }) + } +} + +pub fn expand_imports( + input: ImportMacroInput, + rename_1p_crates: bool, +) -> std::result::Result> { + let (statements, errs): (Vec<_>, Vec<_>) = input + .imports + .into_iter() + .map(|i| i.try_into_statement(rename_1p_crates)) + .partition(|res| res.is_ok()); + + if !errs.is_empty() { + Err(errs.into_iter().map(std::result::Result::unwrap_err).collect()) + } else { + Ok(statements.into_iter().map(std::result::Result::unwrap).collect()) + } +} + +#[cfg(test)] +mod tests { + use crate::*; + use quickcheck::quickcheck; + use syn::parse_quote; + + #[test] + fn test_expand_imports_without_renaming() -> std::result::Result<(), Vec> { + let rename = false; + + // Nothing to do. + let expanded = expand_imports(parse_quote! {}, rename)?; + assert_eq!(expanded.to_string(), ""); + + // Package and a target. + let expanded = expand_imports(parse_quote! { "//some_project:utils"; }, rename)?; + 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"; }, rename)?; + 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; }, rename)?; + 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"; }, rename)?; + 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; }, rename)?; + 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; }, rename)?; + assert_eq!(expanded.to_string(), "extern crate utils as my_utils ;"); + + // A 3P target. + let expanded = + expand_imports(parse_quote! { "//third_party/rust/serde/v1:serde"; }, rename)?; + assert_eq!(expanded.to_string(), "extern crate serde ;"); + + // A 3P target with a no-op alias. + let expanded = + expand_imports(parse_quote! { "//third_party/rust/serde/v1:serde" as serde; }, rename)?; + assert_eq!(expanded.to_string(), "extern crate serde ;"); + + // A 3P target with an alias. + let expanded = expand_imports( + parse_quote! { "//third_party/rust/serde/v1:serde" as my_serde; }, + rename, + )?; + 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"; }, + rename, + )?; + assert_eq!(expanded.to_string(), "extern crate utils ; extern crate serde ;"); + + // Problematic target name. + let expanded = expand_imports(parse_quote! { "//some_project:thing-types"; }, rename)?; + assert_eq!(expanded.to_string(), "extern crate thing_DASH_types ;"); + + // Problematic target name with alias. + let expanded = expand_imports(parse_quote! { "//some_project:thing-types" as types; }, rename)?; + assert_eq!(expanded.to_string(), "extern crate thing_DASH_types as types ;"); + + // Problematic package name. + let expanded = expand_imports(parse_quote! { "//some_project-prototype:utils"; }, rename)?; + assert_eq!(expanded.to_string(), "extern crate utils ;"); + + // Problematic package and target names. + let expanded = expand_imports(parse_quote! { "//some_project-prototype:thing-types"; }, rename)?; + assert_eq!(expanded.to_string(), "extern crate thing_DASH_types ;"); + + Ok(()) + } + + #[test] + fn test_expand_imports_with_renaming() -> std::result::Result<(), Vec> { + let rename = true; + + // Nothing to do. + let expanded = expand_imports(parse_quote! {}, rename)?; + assert_eq!(expanded.to_string(), ""); + + // Package and a target. + let expanded = expand_imports(parse_quote! { "//some_project:utils"; }, rename)?; + assert_eq!(expanded.to_string(), "extern crate some_project_COLON_utils as utils ;"); + + // Package and a target, with a no-op alias. + let expanded = expand_imports(parse_quote! { "//some_project:utils" as utils; }, rename)?; + assert_eq!(expanded.to_string(), "extern crate some_project_COLON_utils as utils ;"); + + // Package and a target, with an alias. + let expanded = expand_imports(parse_quote! { "//some_project:utils" as my_utils; }, rename)?; + assert_eq!(expanded.to_string(), "extern crate some_project_COLON_utils as my_utils ;"); + + // Package and an implicit target. + let expanded = expand_imports(parse_quote! { "//some_project/utils"; }, rename)?; + assert_eq!(expanded.to_string(), "extern crate some_project_SLASH_utils_COLON_utils as utils ;"); + + // Package and an implicit target, with a no-op alias. + let expanded = expand_imports(parse_quote! { "//some_project/utils" as utils; }, rename)?; + assert_eq!(expanded.to_string(), "extern crate some_project_SLASH_utils_COLON_utils as utils ;"); + + // Package and an implicit target, with an alias. + let expanded = expand_imports(parse_quote! { "//some_project/utils" as my_utils; }, rename)?; + assert_eq!( + expanded.to_string(), + "extern crate some_project_SLASH_utils_COLON_utils as my_utils ;" + ); + + // A 3P target. + let expanded = + expand_imports(parse_quote! { "//third_party/rust/serde/v1:serde"; }, rename)?; + assert_eq!(expanded.to_string(), "extern crate serde ;"); + + // A 3P target with a no-op alias. + let expanded = + expand_imports(parse_quote! { "//third_party/rust/serde/v1:serde" as serde; }, rename)?; + assert_eq!(expanded.to_string(), "extern crate serde ;"); + + // A 3P target with an alias. + let expanded = expand_imports( + parse_quote! { "//third_party/rust/serde/v1:serde" as my_serde; }, + rename, + )?; + 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"; }, + rename, + )?; + assert_eq!( + expanded.to_string(), + "extern crate some_project_COLON_utils as utils ; extern crate serde ;" + ); + + // Problematic target name. + let expanded = expand_imports(parse_quote! { "//some_project:thing-types"; }, rename)?; + assert_eq!( + expanded.to_string(), + "extern crate some_project_COLON_thing_DASH_types as thing_DASH_types ;" + ); + + // Problematic target name with alias. + let expanded = expand_imports(parse_quote! { "//some_project:thing-types" as types; }, rename)?; + assert_eq!(expanded.to_string(), "extern crate some_project_COLON_thing_DASH_types as types ;"); + + // Problematic package name. + let expanded = expand_imports(parse_quote! { "//some_project-prototype:utils"; }, rename)?; + assert_eq!( + expanded.to_string(), + "extern crate some_project_DASH_prototype_COLON_utils as utils ;" + ); + + // Problematic package and target names. + let expanded = expand_imports(parse_quote! { "//some_project-prototype:thing-types"; }, rename)?; + assert_eq!( + expanded.to_string(), + "extern crate some_project_DASH_prototype_COLON_thing_DASH_types as thing_DASH_types ;" + ); + + Ok(()) + } + + #[test] + fn test_expansion_failures() -> Result<()> { + let rename = false; + + // Missing leading "//", not a valid label. + let errs = expand_imports(parse_quote! { "some_project:utils"; }, rename).unwrap_err(); + assert_eq!( + errs.into_iter().map(|e| e.to_string()).collect::>(), + vec!["Bazel labels must be of the form '//package[:target]'"] + ); + + // Valid label, but relative. + let errs = expand_imports(parse_quote! { ":utils"; }, rename).unwrap_err(); + assert_eq!( + errs.into_iter().map(|e| e.to_string()).collect::>(), + vec!["Bazel labels must be of the form '//package[:target]'"] + ); + + // Valid label, but a wildcard. + let errs = expand_imports(parse_quote! { "some_project/..."; }, rename).unwrap_err(); + assert_eq!( + errs.into_iter().map(|e| e.to_string()).collect::>(), + 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::("some_project:utils;").unwrap_err().to_string(), + "expected Bazel label as a string literal" + ); + + // Alias is not a valid identifier. + assert_eq!( + syn::parse_str::(r#""some_project:utils" as "!@#$%";"#) + .unwrap_err() + .to_string(), + "alias must be a valid Rust identifier" + ); + + Ok(()) + } + + #[test] + fn test_label_parsing() -> Result<()> { + assert_eq!( + BazelLabel::parse("//some_project:utils", &Span::call_site())?, + BazelLabel { package: "some_project".to_string(), target: "utils".to_string() }, + ); + assert_eq!( + BazelLabel::parse("//some_project/utils", &Span::call_site())?, + BazelLabel { package: "some_project/utils".to_string(), target: "utils".to_string() }, + ); + assert_eq!( + BazelLabel::parse("//some_project", &Span::call_site())?, + BazelLabel { package: "some_project".to_string(), target: "some_project".to_string() }, + ); + + Ok(()) + } + + #[test] + fn test_mangle() -> Result<()> { + assert_eq!(mangle("some_project:utils"), "some_project_COLON_utils"); + assert_eq!(&mangle("_QUOTEDOT_"), "_QUOTEQUOTE_DOT_"); + + Ok(()) + } + + #[test] + fn test_unmangle() -> Result<()> { + assert_eq!(unmangle("some_project_COLON_utils"), "some_project:utils"); + assert_eq!(unmangle("_QUOTEQUOTE_DOT_"), "_QUOTEDOT_"); + + Ok(()) + } + + #[test] + fn test_substitutions_compose() -> Result<()> { + for s in SUBSTITUTIONS.0.iter().chain(SUBSTITUTIONS.1.iter()) { + assert_eq!(&unmangle(&mangle(s)), s); + } + + Ok(()) + } + + quickcheck! { + fn composition_is_identity(s: String) -> bool { + s == unmangle(&mangle(&s)) + } + } +} \ No newline at end of file