use syn::parse::Parser;
use super::*;
pub(super) const PATH_MACROS: [&str; 3] = ["include", "include_str", "include_bytes"];
pub(super) fn rebase_includes(contents: &str, levels: usize, path: &str) -> String {
let Ok(stream) = contents.parse::<proc_macro2::TokenStream>() else {
return contents.to_owned();
};
let mut edits = Vec::new();
let prefix = "../".repeat(levels);
collect_include_edits(stream, &prefix, contents, path, &mut edits);
apply_replacements(contents, &edits)
}
fn collect_include_edits<'a>(
stream: proc_macro2::TokenStream,
prefix: &'a str,
contents: &str,
path: &str,
edits: &mut Vec<(std::ops::Range<usize>, &'a str)>,
) {
let trees: Vec<proc_macro2::TokenTree> = stream.into_iter().collect();
for (index, tree) in trees.iter().enumerate() {
let proc_macro2::TokenTree::Group(group) = tree else {
continue;
};
if !is_path_macro_call(&trees, index) {
collect_include_edits(group.stream(), prefix, contents, path, edits);
continue;
}
match literal_insert_point(&group.stream()) {
Some(Some(offset)) => edits.push((offset..offset, prefix)),
Some(None) => {}
None => {
let line = contents[..group.span().byte_range().start].lines().count();
eprintln!(
"rust-split: {path}:{line}: {}! argument is not a plain string \
literal — its path was not re-based",
trees[index - 2]
);
}
}
}
}
fn is_path_macro_call(trees: &[proc_macro2::TokenTree], index: usize) -> bool {
if index < 2 {
return false;
}
let proc_macro2::TokenTree::Ident(name) = &trees[index - 2] else {
return false;
};
let proc_macro2::TokenTree::Punct(bang) = &trees[index - 1] else {
return false;
};
bang.as_char() == '!' && PATH_MACROS.iter().any(|m| name == m)
}
fn literal_insert_point(stream: &proc_macro2::TokenStream) -> Option<Option<usize>> {
let mut trees = stream.clone().into_iter();
let proc_macro2::TokenTree::Literal(literal) = trees.next()? else {
return None;
};
if let Some(token) = trees.next()
&& (!matches!(token, proc_macro2::TokenTree::Punct(p) if p.as_char() == ',')
|| trees.next().is_some())
{
return None;
}
let text = literal.to_string();
let value = syn::parse_str::<syn::LitStr>(&text).ok()?.value();
let windows_drive =
matches!(value.as_bytes(), [drive, b':', b'/' | b'\\', ..] if drive.is_ascii_alphabetic());
if value.is_empty() || value.starts_with('/') || value.starts_with(r"\\") || windows_drive {
return Some(None);
}
let quote = text.find('"')?;
Some(Some(literal.span().byte_range().start + quote + 1))
}
pub(super) fn rebase_body_paths(body: &str, rebase: Rebase) -> String {
if matches!(rebase, Rebase::Keep) {
return body.to_owned();
}
let Ok(file) = syn::parse_file(body) else {
return body.to_owned();
};
let mut visitor = BodyPathEdits {
rebase,
edits: Vec::new(),
};
syn::visit::Visit::visit_file(&mut visitor, &file);
apply_replacements(body, &visitor.edits)
}
struct BodyPathEdits {
rebase: Rebase,
edits: Vec<(std::ops::Range<usize>, &'static str)>,
}
impl<'ast> syn::visit::Visit<'ast> for BodyPathEdits {
fn visit_path(&mut self, path: &'ast syn::Path) {
if path.leading_colon.is_none() && path.segments.len() > 1 {
collect_anchor_edit(&path.segments[0].ident, self.rebase, &mut self.edits);
}
syn::visit::visit_path(self, path);
}
fn visit_item_use(&mut self, item: &'ast syn::ItemUse) {
if item.leading_colon.is_none() {
collect_rebase_edits(&item.tree, self.rebase, &mut self.edits);
}
}
fn visit_macro(&mut self, mac: &'ast syn::Macro) {
syn::visit::Visit::visit_path(self, &mac.path);
let Some(name) = mac.path.segments.last().map(|segment| &segment.ident) else {
return;
};
let is_standard_path = mac.path.segments.len() == 1
|| (mac.path.segments.len() == 2
&& mac
.path
.segments
.first()
.is_some_and(|segment| segment.ident == "std" || segment.ident == "core"));
if !is_standard_path
|| !matches!(
name.to_string().as_str(),
"assert"
| "assert_eq"
| "assert_ne"
| "debug_assert"
| "debug_assert_eq"
| "debug_assert_ne"
| "format"
| "format_args"
| "format_args_nl"
| "panic"
| "print"
| "println"
| "eprint"
| "eprintln"
| "write"
| "writeln"
)
{
return;
}
let parser = syn::punctuated::Punctuated::<syn::Expr, syn::Token![,]>::parse_terminated;
let Ok(arguments) = parser.parse2(mac.tokens.clone()) else {
return;
};
for argument in &arguments {
syn::visit::Visit::visit_expr(self, argument);
}
}
fn visit_attribute(&mut self, _: &'ast syn::Attribute) {}
fn visit_visibility(&mut self, _: &'ast syn::Visibility) {}
}