use std::{
borrow::Cow,
env,
error::Error,
fmt,
io::{self, Write as _},
ops::BitAnd,
path::PathBuf,
process::{Command, Stdio},
str,
sync::Arc,
};
use proc_macro2::Span;
use quote::ToTokens as _;
use regex::RegexSet;
use syn::{
parse_quote, punctuated::Punctuated, token, visit::Visit, Abi, Attribute, Expr, ExprLit,
ForeignItemFn, Generics, Item, ItemTrait, Lit, LitStr, Meta, MetaNameValue, Token, TraitItem,
TraitItemFn, Visibility,
};
pub fn seesaw<'a>(
traits: impl Into<TraitSet>,
bindings: impl fmt::Display,
dest: impl Into<Destination<'a>>,
) -> io::Result<()> {
let items = _seesaw(traits.into(), bindings.to_string())?;
let file = syn::File {
shebang: None,
attrs: vec![],
items: items.into_iter().map(Item::Trait).collect(),
};
let mut rustfmt = match env::var_os("RUSTFMT") {
Some(it) => Command::new(it),
None => Command::new("rustfmt"),
};
let rustfmt = match rustfmt
.stdin(Stdio::piped())
.stderr(Stdio::null())
.stdout(Stdio::piped())
.spawn()
{
Ok(mut child) => {
let mut stdin = child.stdin.take().unwrap();
let ts = file.to_token_stream();
match fmt::write(&mut Write2Write(&mut stdin), format_args!("{ts}")).is_err()
|| stdin.flush().is_err()
{
true => None,
false => {
drop(stdin);
match child.wait_with_output() {
Ok(out) if out.status.success() => Some(out.stdout),
_ => None,
}
}
}
}
Err(_) => None,
};
let formatted = rustfmt.unwrap_or_else(|| Vec::from(prettyplease::unparse(&file)));
let mut writer = match dest.into() {
Destination::Writer(write) => write,
Destination::Path(it) => Box::new(std::fs::File::create(it)?),
};
match option_env!("CARGO_PKG_VERSION") {
Some(v) => writeln!(writer, "/* this file is @generated by seesaw {v} */\n"),
None => writeln!(writer, "/* this file is @generated by seesaw */\n"),
}?;
io::copy(&mut &formatted[..], &mut writer)?;
writer.flush()
}
pub enum Destination<'a> {
Path(Cow<'a, std::path::Path>),
Writer(Box<dyn io::Write + 'a>),
}
impl<'a> From<&'a std::path::Path> for Destination<'a> {
fn from(value: &'a std::path::Path) -> Self {
Self::Path(Cow::Borrowed(value))
}
}
impl From<PathBuf> for Destination<'_> {
fn from(value: PathBuf) -> Self {
Self::Path(Cow::Owned(value))
}
}
impl<'a> From<&'a str> for Destination<'a> {
fn from(value: &'a str) -> Self {
Self::from(std::path::Path::new(value))
}
}
impl From<String> for Destination<'_> {
fn from(value: String) -> Self {
Self::from(PathBuf::from(value))
}
}
impl<'a> From<&'a mut Vec<u8>> for Destination<'a> {
fn from(value: &'a mut Vec<u8>) -> Self {
Self::Writer(Box::new(value))
}
}
impl<'a> From<&'a mut String> for Destination<'a> {
fn from(value: &'a mut String) -> Self {
Self::Writer(Box::new(Write2Write(value)))
}
}
macro_rules! ref_writer {
($($ty:ty),* $(,)?) => {
$(
impl<'a> From<&'a $ty> for Destination<'a> {
fn from(value: &'a $ty) -> Self {
Self::Writer(Box::new(value))
}
}
)*
};
}
macro_rules! own_writer {
($($ty:ty),* $(,)?) => {
$(
impl From<$ty> for Destination<'_> {
fn from(value: $ty) -> Self {
Self::Writer(Box::new(value))
}
}
)*
};
}
ref_writer! {
io::Empty,
io::Sink,
io::Stderr,
io::Stdout,
std::fs::File,
std::net::TcpStream,
std::process::ChildStdin,
}
own_writer! {
Arc<std::fs::File>,
io::Empty,
io::Sink,
io::Stderr,
io::Stdout,
std::fs::File,
std::io::StderrLock<'static>,
std::io::StdoutLock<'static>,
std::net::TcpStream,
std::process::ChildStdin,
}
fn _seesaw(TraitSet(traits): TraitSet, bindings: String) -> io::Result<Vec<ItemTrait>> {
let bindings = err(
io::ErrorKind::InvalidData,
syn::parse_file(&bindings.to_string()),
)?;
let span = Span::call_site();
traits
.into_iter()
.map(
|Trait {
name,
allowlist,
blocklist,
public,
}| {
Ok(ItemTrait {
attrs: vec![parse_quote!(#[allow(unused)])],
vis: match public {
true => Visibility::Public(Token)),
false => Visibility::Inherited,
},
unsafety: None,
auto_token: None,
restriction: None,
trait_token: Token,
ident: err(io::ErrorKind::InvalidInput, syn::parse_str(&name))?,
generics: Generics::default(),
colon_token: None,
supertraits: Punctuated::new(),
brace_token: token::Brace(span),
items: extract(
&err(io::ErrorKind::InvalidInput, RegexSet::new(allowlist))?,
&err(io::ErrorKind::InvalidInput, RegexSet::new(blocklist))?,
&bindings,
)
.into_iter()
.map(|it| {
let mut sig = it.sig.clone();
sig.unsafety = Some(Token);
sig.abi = Some(Abi {
extern_token: Token,
name: Some(LitStr::new("C", span)),
});
TraitItem::Fn(TraitItemFn {
attrs: it
.attrs
.clone()
.into_iter()
.flat_map(break_comments)
.collect(),
sig,
default: None,
semi_token: Some(it.semi_token),
})
})
.collect(),
})
},
)
.collect()
}
#[derive(Debug, Clone)]
pub struct Trait {
public: bool,
name: String,
allowlist: Vec<String>,
blocklist: Vec<String>,
}
impl Trait {
pub fn new(name: impl Into<String>) -> Self {
Self {
public: true,
name: name.into(),
allowlist: vec![],
blocklist: vec![],
}
}
pub fn private(mut self) -> Self {
self.public = false;
self
}
pub fn allow(self, s: impl Into<String>) -> Self {
self.allow_all([s])
}
pub fn allow_all<S: Into<String>>(mut self, i: impl IntoIterator<Item = S>) -> Self {
self.allowlist.extend(i.into_iter().map(Into::into));
self
}
pub fn block(self, s: impl Into<String>) -> Self {
self.block_all([s])
}
pub fn block_all<S: Into<String>>(mut self, i: impl IntoIterator<Item = S>) -> Self {
self.blocklist.extend(i.into_iter().map(Into::into));
self
}
}
impl BitAnd<Self> for Trait {
type Output = TraitSet;
fn bitand(self, rhs: Self) -> Self::Output {
TraitSet(vec![self, rhs])
}
}
impl BitAnd<TraitSet> for Trait {
type Output = TraitSet;
fn bitand(self, rhs: TraitSet) -> Self::Output {
rhs & self
}
}
#[derive(Debug, Default, Clone)]
pub struct TraitSet(Vec<Trait>);
impl TraitSet {
pub fn new() -> Self {
Self::default()
}
}
impl From<Trait> for TraitSet {
fn from(value: Trait) -> Self {
Self(vec![value])
}
}
impl From<String> for TraitSet {
fn from(value: String) -> Self {
Trait::new(value).into()
}
}
impl From<&str> for TraitSet {
fn from(value: &str) -> Self {
Self::from(String::from(value))
}
}
impl BitAnd<Trait> for TraitSet {
type Output = Self;
fn bitand(mut self, rhs: Trait) -> Self::Output {
self.0.push(rhs);
self
}
}
impl BitAnd<Self> for TraitSet {
type Output = Self;
fn bitand(mut self, mut rhs: Self) -> Self::Output {
self.0.append(&mut rhs.0);
self
}
}
impl Extend<Trait> for TraitSet {
fn extend<T: IntoIterator<Item = Trait>>(&mut self, iter: T) {
self.0.extend(iter);
}
}
impl FromIterator<Trait> for TraitSet {
fn from_iter<T: IntoIterator<Item = Trait>>(iter: T) -> Self {
Self(iter.into_iter().collect())
}
}
fn extract<'ast>(
allowlist: &RegexSet,
blocklist: &RegexSet,
file: &'ast syn::File,
) -> Vec<&'ast ForeignItemFn> {
struct Visitor<'a, 'ast> {
allowlist: &'a RegexSet,
blocklist: &'a RegexSet,
selected: Vec<&'ast ForeignItemFn>,
}
impl<'ast> Visit<'ast> for Visitor<'_, 'ast> {
fn visit_foreign_item_fn(&mut self, i: &'ast ForeignItemFn) {
if allowed(self.allowlist, self.blocklist, &i.sig.ident.to_string()) {
self.selected.push(i)
};
}
}
let mut visitor = Visitor {
allowlist,
blocklist,
selected: vec![],
};
visitor.visit_file(file);
visitor.selected
}
fn allowed(allowlist: &RegexSet, blocklist: &RegexSet, s: &str) -> bool {
match (
allowlist.is_empty(),
allowlist.is_match(s),
blocklist.is_empty(),
blocklist.is_match(s),
) {
(_, _, false, true) => false, (false, true, _, _) => true, (false, false, _, _) => false, (true, _, _, _) => true, }
}
#[test]
fn test_allowed() {
#[track_caller]
fn t(allow: &[&str], block: &[&str], s: &str, expected: bool) {
let allow = &RegexSet::new(allow).unwrap();
let block = &RegexSet::new(block).unwrap();
assert_eq!(
allowed(allow, block, s),
expected,
"allow={allow:?}, block={block:?} on {s}"
)
}
t(&[], &[], "hello", true);
t(&[], &["hello"], "hello", false);
t(&["hello"], &["goodbye"], "hello", true);
}
fn err<T>(
kind: io::ErrorKind,
res: Result<T, impl Error + Send + Sync + 'static>,
) -> io::Result<T> {
match res {
Ok(it) => Ok(it),
Err(e) => Err(io::Error::new(kind, e)),
}
}
struct Write2Write<T>(T);
impl<T: io::Write> fmt::Write for Write2Write<T> {
fn write_str(&mut self, s: &str) -> fmt::Result {
self.0.write_all(s.as_bytes()).map_err(|_| fmt::Error)
}
fn write_fmt(&mut self, args: fmt::Arguments<'_>) -> fmt::Result {
self.0.write_fmt(args).map_err(|_| fmt::Error)
}
}
impl<T: fmt::Write> io::Write for Write2Write<T> {
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
match self
.0
.write_str(err(io::ErrorKind::InvalidData, str::from_utf8(buf))?)
{
Ok(()) => Ok(buf.len()),
Err(fmt::Error) => Err(io::ErrorKind::Other)?,
}
}
fn flush(&mut self) -> io::Result<()> {
Ok(())
}
}
fn break_comments(i: Attribute) -> impl Iterator<Item = Attribute> {
match i.meta {
Meta::NameValue(MetaNameValue {
path,
eq_token,
value:
Expr::Lit(ExprLit {
attrs,
lit: Lit::Str(doc),
}),
}) if path.is_ident("doc") && attrs.is_empty() => doc
.value()
.lines()
.map(|line| Attribute {
pound_token: Token,
style: match &i.style {
syn::AttrStyle::Outer => syn::AttrStyle::Outer,
syn::AttrStyle::Inner(not) => syn::AttrStyle::Inner(Token),
},
bracket_token: token::Bracket(i.bracket_token.span),
meta: Meta::NameValue(MetaNameValue {
path: syn::Path::from(path.get_ident().unwrap().clone()),
eq_token: Token,
value: Expr::Lit(ExprLit {
attrs: vec![],
lit: Lit::Str(LitStr::new(line, doc.span())),
}),
}),
})
.collect::<Vec<_>>()
.into_iter(),
_ => vec![i].into_iter(),
}
}