1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172
|
// Copyright 2023 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
use proc_macro2::Span;
use quote::quote;
use syn::parse::{Parse, ParseStream};
use syn::{parse_macro_input, Error, Ident, Lit, Token};
#[proc_macro]
pub fn import(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
let imports = parse_macro_input!(input as ImportList).imports;
let mut stream = proc_macro2::TokenStream::new();
for i in imports {
let public = &i.reexport;
let mangled_crate_name = &i.target.mangled_crate_name;
let name = i.alias.as_ref().unwrap_or(&i.target.gn_name);
stream.extend(quote! {
#public extern crate #mangled_crate_name as #name;
});
}
stream.into()
}
struct ImportList {
imports: Vec<Import>,
}
struct Import {
target: GnTarget,
alias: Option<Ident>,
reexport: Option<Token![pub]>,
}
struct GnTarget {
mangled_crate_name: Ident,
gn_name: Ident,
}
impl GnTarget {
fn parse(s: &str, span: Span) -> Result<GnTarget, String> {
if !s.starts_with("//") {
return Err(String::from("expected absolute GN path (should start with //)"));
}
let mut path: Vec<&str> = s[2..].split('/').collect();
let gn_name = {
if path.starts_with(&["third_party", "rust"]) {
return Err(String::from(
"import! macro should not be used for third_party crates",
));
}
let last = path.pop().unwrap();
let (split_last, gn_name) = match last.split_once(':') {
Some((last, name)) => (last, name),
None => (last, last),
};
path.push(split_last);
gn_name
};
for p in &path {
if p.contains(':') {
return Err(String::from("unexpected ':' in GN path component"));
}
if p.is_empty() {
return Err(String::from("unexpected empty GN path component"));
}
}
let mangled_crate_name =
escape_non_identifier_chars(&format!("{}:{gn_name}", path.join("/")))?;
Ok(GnTarget {
mangled_crate_name: Ident::new(&mangled_crate_name, span),
gn_name: syn::parse_str::<Ident>(gn_name).map_err(|e| format!("{e}"))?,
})
}
}
impl Parse for ImportList {
fn parse(input: ParseStream) -> syn::Result<Self> {
let mut imports: Vec<Import> = Vec::new();
while !input.is_empty() {
let reexport = <Token![pub]>::parse(input).ok();
let str_span = input.span();
let target = match Lit::parse(input) {
Err(_) => {
return Err(Error::new(str_span, "expected a GN path as a string literal"));
}
Ok(Lit::Str(label)) => match GnTarget::parse(&label.value(), str_span) {
Ok(target) => target,
Err(e) => {
return Err(Error::new(
str_span,
format!("invalid GN path {}: {}", quote::quote! {#label}, e),
));
}
},
Ok(lit) => {
return Err(Error::new(
str_span,
format!(
"expected a GN path as string literal, found '{}' literal",
quote::quote! {#lit}
),
));
}
};
let alias = match <Token![as]>::parse(input) {
Ok(_) => Some(Ident::parse(input)?),
Err(_) => None,
};
<syn::Token![;]>::parse(input)?;
imports.push(Import { target, alias, reexport });
}
Ok(Self { imports })
}
}
/// Escapes non-identifier characters in `symbol`.
///
/// Importantly, this is
/// [an injective function](https://en.wikipedia.org/wiki/Injective_function)
/// which means that different inputs are never mapped to the same output.
///
/// This is based on a similar function in
/// https://github.com/google/crubit/blob/22ab04aef9f7cc56d8600c310c7fe20999ffc41b/common/code_gen_utils.rs#L59-L71
/// The main differences are:
///
/// * Only a limited set of special characters is supported, because this makes
/// it easier to replicate the escaping algorithm in `.gni` files, using just
/// `string_replace` calls.
/// * No dependency on `unicode_ident` crate means that instead of
/// `is_xid_continue` a more restricted call to `char::is_ascii_alphanumeric`
/// is used.
/// * No support for escaping leading digits.
/// * The escapes are slightly different (e.g. `/` frequently appears in GN
/// paths and therefore here we map it to a nice `_s` rather than to `_x002f`)
fn escape_non_identifier_chars(symbol: &str) -> Result<String, String> {
assert!(!symbol.is_empty()); // Caller is expected to verify.
if symbol.chars().next().unwrap().is_ascii_digit() {
return Err("Leading digits are not supported".to_string());
}
// Escaping every character can at most double the size of the string.
let mut result = String::with_capacity(symbol.len() * 2);
for c in symbol.chars() {
// NOTE: TargetName=>CrateName mangling algorithm should be updated
// simultaneously in 3 places: here, //build/rust/rust_target.gni,
// //build/rust/rust_static_library.gni.
match c {
'_' => result.push_str("_u"),
'/' => result.push_str("_s"),
':' => result.push_str("_c"),
'-' => result.push_str("_d"),
c if c.is_ascii_alphanumeric() => result.push(c),
_ => return Err(format!("Unsupported character in GN path component: `{c}`")),
}
}
Ok(result)
}
|