blob: 1d44fe634c8b4725ae0d74f39df01a204ee25de2 [file] [log] [blame]
Brian Silvermancc09f182022-03-09 15:40:20 -08001use std::iter;
2use std::vec;
3
4use aho_corasick::AhoCorasick;
5use lazy_static::lazy_static;
6use proc_macro2::{Span, TokenStream};
7use quote::quote_spanned;
8use syn::parse::{Parse, ParseStream};
9use syn::{Error, Ident, Lit, LitStr, Result, Token};
10
11/// The possible renaming modes for this macro.
12pub 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)]
25struct AbsoluteLabel<'s> {
26 package_name: &'s str,
27 name: &'s str,
28}
29
30impl<'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
75lazy_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.
133fn encode(s: &str) -> String {
134 ENCODER.replace_all(s, &SUBSTITUTIONS.1)
135}
136
137struct Import {
138 label: LitStr,
139 alias: Option<Ident>,
140}
141
142impl 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
167pub struct ImportMacroInput {
168 imports: Vec<Import>,
169}
170
171impl 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
204pub 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)]
222mod 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}