Brian Silverman | cc09f18 | 2022-03-09 15:40:20 -0800 | [diff] [blame^] | 1 | use std::iter; |
| 2 | use std::vec; |
| 3 | |
| 4 | use aho_corasick::AhoCorasick; |
| 5 | use lazy_static::lazy_static; |
| 6 | use proc_macro2::{Span, TokenStream}; |
| 7 | use quote::quote_spanned; |
| 8 | use syn::parse::{Parse, ParseStream}; |
| 9 | use syn::{Error, Ident, Lit, LitStr, Result, Token}; |
| 10 | |
| 11 | /// The possible renaming modes for this macro. |
| 12 | pub enum Mode { |
| 13 | /// No renaming will be done; the expansion will replace each label with |
| 14 | /// just the target. |
| 15 | NoRenaming, |
| 16 | /// First-party crates will be renamed, and third-party crates will not be. |
| 17 | /// The expansion will replace first-party labels with an encoded version, |
| 18 | /// and third-party labels with just their target. |
| 19 | RenameFirstPartyCrates { third_party_dir: String }, |
| 20 | } |
| 21 | |
| 22 | /// A special case of label::Label, which must be absolute and must not specify |
| 23 | /// a repository. |
| 24 | #[derive(Debug, PartialEq)] |
| 25 | struct AbsoluteLabel<'s> { |
| 26 | package_name: &'s str, |
| 27 | name: &'s str, |
| 28 | } |
| 29 | |
| 30 | impl<'s> AbsoluteLabel<'s> { |
| 31 | /// Parses a string as an absolute Bazel label. Labels must be for the |
| 32 | /// current repository. |
| 33 | fn parse(label: &'s str, span: &'s Span) -> Result<Self> { |
| 34 | if let Ok(label::Label { |
| 35 | repository_name: None, |
| 36 | package_name: Some(package_name), |
| 37 | name, |
| 38 | }) = label::analyze(label) |
| 39 | { |
| 40 | Ok(AbsoluteLabel { package_name, name }) |
| 41 | } else { |
| 42 | Err(Error::new( |
| 43 | *span, |
| 44 | "Bazel labels must be of the form '//package[:target]'", |
| 45 | )) |
| 46 | } |
| 47 | } |
| 48 | |
| 49 | /// Returns true iff this label should be renamed. |
| 50 | fn should_rename(&self, mode: &Mode) -> bool { |
| 51 | match mode { |
| 52 | Mode::NoRenaming => false, |
| 53 | Mode::RenameFirstPartyCrates { third_party_dir } => { |
| 54 | !self.package_name.starts_with(third_party_dir) |
| 55 | } |
| 56 | } |
| 57 | } |
| 58 | |
| 59 | /// Returns the appropriate (encoded) alias to use, if this label is being |
| 60 | /// renamed; otherwise, returns None. |
| 61 | fn target_as_alias(&self, mode: &Mode) -> Option<String> { |
| 62 | self.should_rename(mode).then(|| encode(self.name)) |
| 63 | } |
| 64 | |
| 65 | /// Returns the full crate name, encoded if necessary. |
| 66 | fn crate_name(&self, mode: &Mode) -> String { |
| 67 | if self.should_rename(mode) { |
| 68 | encode(&format!("{}:{}", self.package_name, self.name)) |
| 69 | } else { |
| 70 | self.name.to_string() |
| 71 | } |
| 72 | } |
| 73 | } |
| 74 | |
| 75 | lazy_static! { |
| 76 | // Transformations are stored as "(unencoded, encoded)" tuples. |
| 77 | // Target names can include: |
| 78 | // !%-@^_` "#$&'()*-+,;<=>?[]{|}~/. |
| 79 | // |
| 80 | // Package names are alphanumeric, plus [_/-]. |
| 81 | // |
| 82 | // Packages and targets are separated by colons. |
| 83 | static ref SUBSTITUTIONS: (Vec<String>, Vec<String>) = |
| 84 | iter::once(("_quote".to_string(), "_quotequote_".to_string())) |
| 85 | .chain( |
| 86 | vec![ |
| 87 | (":", "colon"), |
| 88 | ("!", "bang"), |
| 89 | ("%", "percent"), |
| 90 | ("@", "at"), |
| 91 | ("^", "caret"), |
| 92 | ("`", "backtick"), |
| 93 | (" ", "space"), |
| 94 | ("\"", "quote"), |
| 95 | ("#", "hash"), |
| 96 | ("$", "dollar"), |
| 97 | ("&", "ampersand"), |
| 98 | ("'", "backslash"), |
| 99 | ("(", "lparen"), |
| 100 | (")", "rparen"), |
| 101 | ("*", "star"), |
| 102 | ("-", "dash"), |
| 103 | ("+", "plus"), |
| 104 | (",", "comma"), |
| 105 | (";", "semicolon"), |
| 106 | ("<", "langle"), |
| 107 | ("=", "equal"), |
| 108 | (">", "rangle"), |
| 109 | ("?", "question"), |
| 110 | ("[", "lbracket"), |
| 111 | ("]", "rbracket"), |
| 112 | ("{", "lbrace"), |
| 113 | ("|", "pipe"), |
| 114 | ("}", "rbrace"), |
| 115 | ("~", "tilde"), |
| 116 | ("/", "slash"), |
| 117 | (".", "dot"), |
| 118 | ].into_iter() |
| 119 | .flat_map(|pair| { |
| 120 | vec![ |
| 121 | (format!("_{}_", &pair.1), format!("_quote{}_", &pair.1)), |
| 122 | (pair.0.to_string(), format!("_{}_", &pair.1)), |
| 123 | ].into_iter() |
| 124 | }) |
| 125 | ) |
| 126 | .unzip(); |
| 127 | |
| 128 | static ref ENCODER: AhoCorasick = AhoCorasick::new(&SUBSTITUTIONS.0); |
| 129 | static ref DECODER: AhoCorasick = AhoCorasick::new(&SUBSTITUTIONS.1); |
| 130 | } |
| 131 | |
| 132 | /// Encodes a string using the above encoding scheme. |
| 133 | fn encode(s: &str) -> String { |
| 134 | ENCODER.replace_all(s, &SUBSTITUTIONS.1) |
| 135 | } |
| 136 | |
| 137 | struct Import { |
| 138 | label: LitStr, |
| 139 | alias: Option<Ident>, |
| 140 | } |
| 141 | |
| 142 | impl Import { |
| 143 | fn try_into_statement(self, mode: &Mode) -> Result<proc_macro2::TokenStream> { |
| 144 | let label_literal = &self.label.value(); |
| 145 | let span = self.label.span(); |
| 146 | let label = AbsoluteLabel::parse(label_literal, &span)?; |
| 147 | let crate_name = &label.crate_name(mode); |
| 148 | |
| 149 | let crate_ident = Ident::new(crate_name, span); |
| 150 | let alias = self |
| 151 | .alias |
| 152 | .or_else(|| { |
| 153 | label |
| 154 | .target_as_alias(mode) |
| 155 | .map(|alias| Ident::new(&alias, span)) |
| 156 | }) |
| 157 | .filter(|alias| alias != crate_name); |
| 158 | |
| 159 | Ok(if let Some(alias) = alias { |
| 160 | quote_spanned! {span=> extern crate #crate_ident as #alias; } |
| 161 | } else { |
| 162 | quote_spanned! {span=> extern crate #crate_ident;} |
| 163 | }) |
| 164 | } |
| 165 | } |
| 166 | |
| 167 | pub struct ImportMacroInput { |
| 168 | imports: Vec<Import>, |
| 169 | } |
| 170 | |
| 171 | impl Parse for ImportMacroInput { |
| 172 | fn parse(input: ParseStream) -> Result<Self> { |
| 173 | let mut imports: Vec<Import> = Vec::new(); |
| 174 | |
| 175 | while !input.is_empty() { |
| 176 | let label = match Lit::parse(input) |
| 177 | .map_err(|_| input.error("expected Bazel label as a string literal"))? |
| 178 | { |
| 179 | Lit::Str(label) => label, |
| 180 | lit => { |
| 181 | return Err(input.error(format!( |
| 182 | "expected Bazel label as string literal, found '{}' literal", |
| 183 | quote::quote! {#lit} |
| 184 | ))); |
| 185 | } |
| 186 | }; |
| 187 | let alias = if input.peek(Token![as]) { |
| 188 | <Token![as]>::parse(input)?; |
| 189 | Some( |
| 190 | Ident::parse(input) |
| 191 | .map_err(|_| input.error("alias must be a valid Rust identifier"))?, |
| 192 | ) |
| 193 | } else { |
| 194 | None |
| 195 | }; |
| 196 | imports.push(Import { label, alias }); |
| 197 | <syn::Token![;]>::parse(input)?; |
| 198 | } |
| 199 | |
| 200 | Ok(Self { imports }) |
| 201 | } |
| 202 | } |
| 203 | |
| 204 | pub fn expand_imports( |
| 205 | input: ImportMacroInput, |
| 206 | mode: &Mode, |
| 207 | ) -> std::result::Result<TokenStream, Vec<syn::Error>> { |
| 208 | let (statements, errs): (Vec<_>, Vec<_>) = input |
| 209 | .imports |
| 210 | .into_iter() |
| 211 | .map(|i| i.try_into_statement(mode)) |
| 212 | .partition(Result::is_ok); |
| 213 | |
| 214 | if !errs.is_empty() { |
| 215 | Err(errs.into_iter().map(Result::unwrap_err).collect()) |
| 216 | } else { |
| 217 | Ok(statements.into_iter().map(Result::unwrap).collect()) |
| 218 | } |
| 219 | } |
| 220 | |
| 221 | #[cfg(test)] |
| 222 | mod tests { |
| 223 | use crate::*; |
| 224 | use quickcheck::quickcheck; |
| 225 | use syn::parse_quote; |
| 226 | |
| 227 | /// Decodes a string that was encoded using `encode`. |
| 228 | fn decode(s: &str) -> String { |
| 229 | DECODER.replace_all(s, &SUBSTITUTIONS.0) |
| 230 | } |
| 231 | |
| 232 | #[test] |
| 233 | fn test_expand_imports_without_renaming() -> std::result::Result<(), Vec<syn::Error>> { |
| 234 | let mode = Mode::NoRenaming; |
| 235 | |
| 236 | // Nothing to do. |
| 237 | let expanded = expand_imports(parse_quote! {}, &mode)?; |
| 238 | assert_eq!(expanded.to_string(), ""); |
| 239 | |
| 240 | // Package and a target. |
| 241 | let expanded = expand_imports(parse_quote! { "//some_project:utils"; }, &mode)?; |
| 242 | assert_eq!(expanded.to_string(), "extern crate utils ;"); |
| 243 | |
| 244 | // Package and a target, with a no-op alias. |
| 245 | let expanded = expand_imports(parse_quote! { "//some_project:utils"; }, &mode)?; |
| 246 | assert_eq!(expanded.to_string(), "extern crate utils ;"); |
| 247 | |
| 248 | // Package and a target, with an alias. |
| 249 | let expanded = expand_imports(parse_quote! { "//some_project:utils" as my_utils; }, &mode)?; |
| 250 | assert_eq!(expanded.to_string(), "extern crate utils as my_utils ;"); |
| 251 | |
| 252 | // Package and an implicit target. |
| 253 | let expanded = expand_imports(parse_quote! { "//some_project/utils"; }, &mode)?; |
| 254 | assert_eq!(expanded.to_string(), "extern crate utils ;"); |
| 255 | |
| 256 | // Package and an implicit target, with a no-op alias. |
| 257 | let expanded = expand_imports(parse_quote! { "//some_project/utils" as utils; }, &mode)?; |
| 258 | assert_eq!(expanded.to_string(), "extern crate utils ;"); |
| 259 | |
| 260 | // Package and an implicit target, with an alias. |
| 261 | let expanded = expand_imports(parse_quote! { "//some_project:utils" as my_utils; }, &mode)?; |
| 262 | assert_eq!(expanded.to_string(), "extern crate utils as my_utils ;"); |
| 263 | |
| 264 | // A third-party target. |
| 265 | let expanded = |
| 266 | expand_imports(parse_quote! { "//third_party/rust/serde/v1:serde"; }, &mode)?; |
| 267 | assert_eq!(expanded.to_string(), "extern crate serde ;"); |
| 268 | |
| 269 | // A third-party target with a no-op alias. |
| 270 | let expanded = expand_imports( |
| 271 | parse_quote! { "//third_party/rust/serde/v1:serde" as serde; }, |
| 272 | &mode, |
| 273 | )?; |
| 274 | assert_eq!(expanded.to_string(), "extern crate serde ;"); |
| 275 | |
| 276 | // A third-party target with an alias. |
| 277 | let expanded = expand_imports( |
| 278 | parse_quote! { "//third_party/rust/serde/v1:serde" as my_serde; }, |
| 279 | &mode, |
| 280 | )?; |
| 281 | assert_eq!(expanded.to_string(), "extern crate serde as my_serde ;"); |
| 282 | |
| 283 | // Multiple targets. |
| 284 | let expanded = expand_imports( |
| 285 | parse_quote! { "//some_project:utils"; "//third_party/rust/serde/v1:serde"; }, |
| 286 | &mode, |
| 287 | )?; |
| 288 | assert_eq!( |
| 289 | expanded.to_string(), |
| 290 | "extern crate utils ; extern crate serde ;" |
| 291 | ); |
| 292 | |
| 293 | Ok(()) |
| 294 | } |
| 295 | |
| 296 | #[test] |
| 297 | fn test_expand_imports_with_renaming() -> std::result::Result<(), Vec<syn::Error>> { |
| 298 | let mode = Mode::RenameFirstPartyCrates { |
| 299 | third_party_dir: "third_party/rust".to_string(), |
| 300 | }; |
| 301 | |
| 302 | // Nothing to do. |
| 303 | let expanded = expand_imports(parse_quote! {}, &mode)?; |
| 304 | assert_eq!(expanded.to_string(), ""); |
| 305 | |
| 306 | // Package and a target. |
| 307 | let expanded = expand_imports(parse_quote! { "//some_project:utils"; }, &mode)?; |
| 308 | assert_eq!( |
| 309 | expanded.to_string(), |
| 310 | "extern crate some_project_colon_utils as utils ;" |
| 311 | ); |
| 312 | |
| 313 | // Package and a target, with a no-op alias. |
| 314 | let expanded = expand_imports(parse_quote! { "//some_project:utils" as utils; }, &mode)?; |
| 315 | assert_eq!( |
| 316 | expanded.to_string(), |
| 317 | "extern crate some_project_colon_utils as utils ;" |
| 318 | ); |
| 319 | |
| 320 | // Package and a target, with an alias. |
| 321 | let expanded = expand_imports(parse_quote! { "//some_project:utils" as my_utils; }, &mode)?; |
| 322 | assert_eq!( |
| 323 | expanded.to_string(), |
| 324 | "extern crate some_project_colon_utils as my_utils ;" |
| 325 | ); |
| 326 | |
| 327 | // Package and an implicit target. |
| 328 | let expanded = expand_imports(parse_quote! { "//some_project/utils"; }, &mode)?; |
| 329 | assert_eq!( |
| 330 | expanded.to_string(), |
| 331 | "extern crate some_project_slash_utils_colon_utils as utils ;" |
| 332 | ); |
| 333 | |
| 334 | // Package and an implicit target, with a no-op alias. |
| 335 | let expanded = expand_imports(parse_quote! { "//some_project/utils" as utils; }, &mode)?; |
| 336 | assert_eq!( |
| 337 | expanded.to_string(), |
| 338 | "extern crate some_project_slash_utils_colon_utils as utils ;" |
| 339 | ); |
| 340 | |
| 341 | // Package and an implicit target, with an alias. |
| 342 | let expanded = expand_imports(parse_quote! { "//some_project/utils" as my_utils; }, &mode)?; |
| 343 | assert_eq!( |
| 344 | expanded.to_string(), |
| 345 | "extern crate some_project_slash_utils_colon_utils as my_utils ;" |
| 346 | ); |
| 347 | |
| 348 | // A third-party target. |
| 349 | let expanded = |
| 350 | expand_imports(parse_quote! { "//third_party/rust/serde/v1:serde"; }, &mode)?; |
| 351 | assert_eq!(expanded.to_string(), "extern crate serde ;"); |
| 352 | |
| 353 | // A third-party target with a no-op alias. |
| 354 | let expanded = expand_imports( |
| 355 | parse_quote! { "//third_party/rust/serde/v1:serde" as serde; }, |
| 356 | &mode, |
| 357 | )?; |
| 358 | assert_eq!(expanded.to_string(), "extern crate serde ;"); |
| 359 | |
| 360 | // A third-party target with an alias. |
| 361 | let expanded = expand_imports( |
| 362 | parse_quote! { "//third_party/rust/serde/v1:serde" as my_serde; }, |
| 363 | &mode, |
| 364 | )?; |
| 365 | assert_eq!(expanded.to_string(), "extern crate serde as my_serde ;"); |
| 366 | |
| 367 | // Multiple targets. |
| 368 | let expanded = expand_imports( |
| 369 | parse_quote! { "//some_project:utils"; "//third_party/rust/serde/v1:serde"; }, |
| 370 | &mode, |
| 371 | )?; |
| 372 | assert_eq!( |
| 373 | expanded.to_string(), |
| 374 | "extern crate some_project_colon_utils as utils ; extern crate serde ;" |
| 375 | ); |
| 376 | |
| 377 | // Problematic target name. |
| 378 | let expanded = expand_imports(parse_quote! { "//some_project:thing-types"; }, &mode)?; |
| 379 | assert_eq!( |
| 380 | expanded.to_string(), |
| 381 | "extern crate some_project_colon_thing_dash_types as thing_dash_types ;" |
| 382 | ); |
| 383 | |
| 384 | // Problematic target name with alias. |
| 385 | let expanded = expand_imports( |
| 386 | parse_quote! { "//some_project:thing-types" as types; }, |
| 387 | &mode, |
| 388 | )?; |
| 389 | assert_eq!( |
| 390 | expanded.to_string(), |
| 391 | "extern crate some_project_colon_thing_dash_types as types ;" |
| 392 | ); |
| 393 | |
| 394 | // Problematic package name. |
| 395 | let expanded = expand_imports(parse_quote! { "//some_project-prototype:utils"; }, &mode)?; |
| 396 | assert_eq!( |
| 397 | expanded.to_string(), |
| 398 | "extern crate some_project_dash_prototype_colon_utils as utils ;" |
| 399 | ); |
| 400 | |
| 401 | // Problematic package and target names. |
| 402 | let expanded = expand_imports( |
| 403 | parse_quote! { "//some_project-prototype:thing-types"; }, |
| 404 | &mode, |
| 405 | )?; |
| 406 | assert_eq!( |
| 407 | expanded.to_string(), |
| 408 | "extern crate some_project_dash_prototype_colon_thing_dash_types as thing_dash_types ;" |
| 409 | ); |
| 410 | |
| 411 | Ok(()) |
| 412 | } |
| 413 | |
| 414 | #[test] |
| 415 | fn test_expansion_failures() -> Result<()> { |
| 416 | let mode = Mode::NoRenaming; |
| 417 | |
| 418 | // Missing leading "//", not a valid label. |
| 419 | let errs = expand_imports(parse_quote! { "some_project:utils"; }, &mode).unwrap_err(); |
| 420 | assert_eq!( |
| 421 | errs.into_iter() |
| 422 | .map(|e| e.to_string()) |
| 423 | .collect::<Vec<String>>(), |
| 424 | vec!["Bazel labels must be of the form '//package[:target]'"] |
| 425 | ); |
| 426 | |
| 427 | // Valid label, but relative. |
| 428 | let errs = expand_imports(parse_quote! { ":utils"; }, &mode).unwrap_err(); |
| 429 | assert_eq!( |
| 430 | errs.into_iter() |
| 431 | .map(|e| e.to_string()) |
| 432 | .collect::<Vec<String>>(), |
| 433 | vec!["Bazel labels must be of the form '//package[:target]'"] |
| 434 | ); |
| 435 | |
| 436 | // Valid label, but a wildcard. |
| 437 | let errs = expand_imports(parse_quote! { "some_project/..."; }, &mode).unwrap_err(); |
| 438 | assert_eq!( |
| 439 | errs.into_iter() |
| 440 | .map(|e| e.to_string()) |
| 441 | .collect::<Vec<String>>(), |
| 442 | vec!["Bazel labels must be of the form '//package[:target]'"] |
| 443 | ); |
| 444 | |
| 445 | // Valid label, but only in Bazel (not in Bazel). |
| 446 | let errs = |
| 447 | expand_imports(parse_quote! { "@repository//some_project:utils"; }, &mode).unwrap_err(); |
| 448 | assert_eq!( |
| 449 | errs.into_iter() |
| 450 | .map(|e| e.to_string()) |
| 451 | .collect::<Vec<String>>(), |
| 452 | vec!["Bazel labels must be of the form '//package[:target]'"] |
| 453 | ); |
| 454 | |
| 455 | Ok(()) |
| 456 | } |
| 457 | |
| 458 | #[test] |
| 459 | fn test_macro_input_parsing_errors() -> Result<()> { |
| 460 | // Label is not a string literal. |
| 461 | assert_eq!( |
| 462 | syn::parse_str::<ImportMacroInput>("some_project:utils;") |
| 463 | .err() |
| 464 | .unwrap() |
| 465 | .to_string(), |
| 466 | "expected Bazel label as a string literal" |
| 467 | ); |
| 468 | |
| 469 | // Label is the wrong kind of literal. |
| 470 | assert_eq!( |
| 471 | syn::parse_str::<ImportMacroInput>("true;") |
| 472 | .err() |
| 473 | .unwrap() |
| 474 | .to_string(), |
| 475 | "expected Bazel label as string literal, found 'true' literal" |
| 476 | ); |
| 477 | assert_eq!( |
| 478 | syn::parse_str::<ImportMacroInput>("123;") |
| 479 | .err() |
| 480 | .unwrap() |
| 481 | .to_string(), |
| 482 | "expected Bazel label as string literal, found '123' literal" |
| 483 | ); |
| 484 | |
| 485 | // Alias is not a valid identifier. |
| 486 | assert_eq!( |
| 487 | syn::parse_str::<ImportMacroInput>(r#""some_project:utils" as "!@#$%";"#) |
| 488 | .err() |
| 489 | .unwrap() |
| 490 | .to_string(), |
| 491 | "alias must be a valid Rust identifier" |
| 492 | ); |
| 493 | |
| 494 | Ok(()) |
| 495 | } |
| 496 | |
| 497 | #[test] |
| 498 | fn test_label_parsing() -> Result<()> { |
| 499 | assert_eq!( |
| 500 | AbsoluteLabel::parse("//some_project:utils", &Span::call_site())?, |
| 501 | AbsoluteLabel { |
| 502 | package_name: "some_project", |
| 503 | name: "utils" |
| 504 | }, |
| 505 | ); |
| 506 | assert_eq!( |
| 507 | AbsoluteLabel::parse("//some_project/utils", &Span::call_site())?, |
| 508 | AbsoluteLabel { |
| 509 | package_name: "some_project/utils", |
| 510 | name: "utils" |
| 511 | }, |
| 512 | ); |
| 513 | assert_eq!( |
| 514 | AbsoluteLabel::parse("//some_project", &Span::call_site())?, |
| 515 | AbsoluteLabel { |
| 516 | package_name: "some_project", |
| 517 | name: "some_project" |
| 518 | }, |
| 519 | ); |
| 520 | |
| 521 | Ok(()) |
| 522 | } |
| 523 | |
| 524 | #[test] |
| 525 | fn test_encode() -> Result<()> { |
| 526 | assert_eq!(encode("some_project:utils"), "some_project_colon_utils"); |
| 527 | assert_eq!(&encode("_quotedot_"), "_quotequote_dot_"); |
| 528 | |
| 529 | Ok(()) |
| 530 | } |
| 531 | |
| 532 | #[test] |
| 533 | fn test_decode() -> Result<()> { |
| 534 | assert_eq!(decode("some_project_colon_utils"), "some_project:utils"); |
| 535 | assert_eq!(decode("_quotequote_dot_"), "_quotedot_"); |
| 536 | |
| 537 | Ok(()) |
| 538 | } |
| 539 | |
| 540 | #[test] |
| 541 | fn test_substitutions_compose() -> Result<()> { |
| 542 | for s in SUBSTITUTIONS.0.iter().chain(SUBSTITUTIONS.1.iter()) { |
| 543 | assert_eq!(&decode(&encode(s)), s); |
| 544 | } |
| 545 | |
| 546 | Ok(()) |
| 547 | } |
| 548 | |
| 549 | quickcheck! { |
| 550 | fn composition_is_identity(s: String) -> bool { |
| 551 | s == decode(&encode(&s)) |
| 552 | } |
| 553 | } |
| 554 | } |