Get macro functional with tests

This commit is contained in:
2025-01-10 18:47:52 -05:00
parent 85dce7f39f
commit 5602c66b11
5 changed files with 667 additions and 86 deletions

235
Cargo.lock generated
View File

@@ -2,15 +2,135 @@
# It is not intended for manual editing.
version = 4
[[package]]
name = "aho-corasick"
version = "1.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916"
dependencies = [
"memchr",
]
[[package]]
name = "autocfg"
version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26"
[[package]]
name = "cfg-if"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
name = "comlexr"
version = "0.1.0"
dependencies = [
"proc-macro2",
"quote",
"rstest",
"syn",
]
[[package]]
name = "equivalent"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5"
[[package]]
name = "futures-core"
version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e"
[[package]]
name = "futures-macro"
version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "futures-task"
version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988"
[[package]]
name = "futures-timer"
version = "3.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f288b0a4f20f9a56b5d1da57e2227c661b7b16168e2f72365f57b63326e29b24"
[[package]]
name = "futures-util"
version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81"
dependencies = [
"futures-core",
"futures-macro",
"futures-task",
"pin-project-lite",
"pin-utils",
"slab",
]
[[package]]
name = "glob"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a8d1add55171497b4705a648c6b583acafb01d58050a51727785f0b2c8e0a2b2"
[[package]]
name = "hashbrown"
version = "0.15.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289"
[[package]]
name = "indexmap"
version = "2.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "62f822373a4fe84d4bb149bf54e584a7f4abec90e072ed49cda0edea5b95471f"
dependencies = [
"equivalent",
"hashbrown",
]
[[package]]
name = "memchr"
version = "2.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3"
[[package]]
name = "pin-project-lite"
version = "0.2.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b"
[[package]]
name = "pin-utils"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
[[package]]
name = "proc-macro-crate"
version = "3.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8ecf48c7ca261d60b74ab1a7b20da18bede46776b2e55535cb958eb595c5fa7b"
dependencies = [
"toml_edit",
]
[[package]]
name = "proc-macro2"
version = "1.0.92"
@@ -29,6 +149,95 @@ dependencies = [
"proc-macro2",
]
[[package]]
name = "regex"
version = "1.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191"
dependencies = [
"aho-corasick",
"memchr",
"regex-automata",
"regex-syntax",
]
[[package]]
name = "regex-automata"
version = "0.4.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908"
dependencies = [
"aho-corasick",
"memchr",
"regex-syntax",
]
[[package]]
name = "regex-syntax"
version = "0.8.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c"
[[package]]
name = "relative-path"
version = "1.9.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ba39f3699c378cd8970968dcbff9c43159ea4cfbd88d43c00b22f2ef10a435d2"
[[package]]
name = "rstest"
version = "0.24.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "03e905296805ab93e13c1ec3a03f4b6c4f35e9498a3d5fa96dc626d22c03cd89"
dependencies = [
"futures-timer",
"futures-util",
"rstest_macros",
"rustc_version",
]
[[package]]
name = "rstest_macros"
version = "0.24.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ef0053bbffce09062bee4bcc499b0fbe7a57b879f1efe088d6d8d4c7adcdef9b"
dependencies = [
"cfg-if",
"glob",
"proc-macro-crate",
"proc-macro2",
"quote",
"regex",
"relative-path",
"rustc_version",
"syn",
"unicode-ident",
]
[[package]]
name = "rustc_version"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92"
dependencies = [
"semver",
]
[[package]]
name = "semver"
version = "1.0.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3cb6eb87a131f756572d7fb904f6e7b68633f09cca868c5df1c4b8d1a694bbba"
[[package]]
name = "slab"
version = "0.4.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67"
dependencies = [
"autocfg",
]
[[package]]
name = "syn"
version = "2.0.96"
@@ -40,8 +249,34 @@ dependencies = [
"unicode-ident",
]
[[package]]
name = "toml_datetime"
version = "0.6.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41"
[[package]]
name = "toml_edit"
version = "0.22.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4ae48d6208a266e853d946088ed816055e556cc6028c5e8e2b84d9fa5dd7c7f5"
dependencies = [
"indexmap",
"toml_datetime",
"winnow",
]
[[package]]
name = "unicode-ident"
version = "1.0.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "adb9e6ca4f869e1180728b7950e35922a7fc6397f7b641499e8f3ef06e50dc83"
[[package]]
name = "winnow"
version = "0.6.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "39281189af81c07ec09db316b302a3e67bf9bd7cbf6c820b50e35fee9c2fa980"
dependencies = [
"memchr",
]

View File

@@ -3,7 +3,24 @@ name = "comlexr"
version = "0.1.0"
edition = "2021"
[lib]
proc-macro = true
[dependencies]
proc-macro2 = "1.0.92"
quote = "1.0.38"
syn = { version = "2.0.96", features = ["full"] }
proc-macro2 = "1"
quote = "1"
syn = { version = "2", features = ["full"] }
[dev-dependencies]
rstest = "0.24"
[lints.rust]
unsafe_code = "forbid"
[lints.clippy]
correctness = "deny"
suspicious = "deny"
perf = "deny"
style = "deny"
nursery = "deny"
pedantic = "deny"

83
bacon.toml Normal file
View File

@@ -0,0 +1,83 @@
# This is a configuration file for the bacon tool
#
# Bacon repository: https://github.com/Canop/bacon
# Complete help on configuration: https://dystroy.org/bacon/config/
# You can also check bacon's own bacon.toml file
# as an example: https://github.com/Canop/bacon/blob/main/bacon.toml
default_job = "clippy-all"
[jobs.check]
command = ["cargo", "check", "--color", "always"]
need_stdout = false
default_watch = false
watch = ["src", "Cargo.toml"]
[jobs.check-all]
command = ["cargo", "check", "--all-features", "--color", "always"]
need_stdout = false
default_watch = false
watch = ["src", "Cargo.toml"]
[jobs.clippy]
command = [
"cargo", "clippy",
"--color", "always",
]
need_stdout = false
default_watch = false
watch = ["src", "Cargo.toml"]
[jobs.clippy-all]
command = [
"cargo", "clippy",
"--all-features",
"--color", "always",
]
need_stdout = false
default_watch = false
watch = ["src", "Cargo.toml"]
[jobs.test]
command = [
"cargo", "+nightly", "test", "--color", "always", "--workspace",
"--", "--color", "always",
]
env.RUSTFLAGS="-Zmacro-backtrace"
need_stdout = true
default_watch = false
watch = ["src", "Cargo.toml", "tests"]
[jobs.test-all]
command = [
"cargo", "+nightly", "test", "--all-features", "--color", "always", "--workspace",
"--", "--color", "always"
]
env.RUSTFLAGS="-Zmacro-backtrace"
need_stdout = true
default_watch = false
watch = ["src", "Cargo.toml", "tests"]
[jobs.doc]
command = ["cargo", "doc", "--color", "always", "--no-deps"]
need_stdout = false
default_watch = false
watch = ["src", "Cargo.toml"]
# If the doc compiles, then it opens in your browser and bacon switches
# to the previous job
[jobs.doc-open]
command = ["cargo", "doc", "--color", "always", "--no-deps", "--open"]
need_stdout = false
on_success = "back" # so that we don't open the browser at each change
# You may define here keybindings that would be specific to
# a project, for example a shortcut to launch a specific job.
# Shortcuts to internal functions (scrolling, toggling, etc.)
# should go in your personal global prefs.toml file instead.
[keybindings]
# alt-m = "job:my-job"
c = "job:clippy"
shift-c = "job:clippy-all" # comment this to have 'c' run clippy on only the default target
t = "job:test"
shift-t = "job:test-all"

View File

@@ -1,7 +1,8 @@
use syn::{bracketed, parse::Parse, punctuated::Punctuated, token, Token};
use quote::{quote, ToTokens};
use syn::{bracketed, parse::Parse, parse_macro_input, punctuated::Punctuated, token, Token};
struct Command {
program: Program,
program: Value,
args: Option<Punctuated<Arg, Token![,]>>,
}
@@ -10,119 +11,257 @@ impl Parse for Command {
let program = input.parse()?;
if input.is_empty() {
Ok(Command {
Ok(Self {
program,
args: None,
})
} else {
_ = input.parse::<Token![,]>()?;
Ok(Command {
Ok(Self {
program,
args: Some(input.parse_terminated(Arg::parse, Token![,])?),
args: Some(Punctuated::parse_terminated(input)?),
})
}
}
}
struct Program(syn::LitStr);
impl ToTokens for Command {
fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) {
let Self { program, args } = self;
let program = quote! { ::std::process::Command::new(#program) };
let args = args.as_ref().map(|args| args.iter());
impl Parse for Program {
fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
input.parse().map(Self)
tokens.extend(args.map_or_else(
|| quote! { #program },
|args| {
quote! {
{
let mut command = #program;
#(#args)*
command
}
}
},
));
}
}
enum Arg {
Expression(syn::Expr),
ForIter {
for_key: Token![for],
expr: syn::Expr,
},
ForInIterMulti {
for_iter: ForIter,
arrow_token: Token![=>],
args: MultiArg,
},
ForInIterSingle {
for_iter: ForIter,
arrow_token: Token![=>],
arg: SingleArg,
},
IfLetMulti {
if_let: IfLet,
arrow_token: Token![=>],
args: MultiArg,
},
IfLetSingle {
if_let: IfLet,
arrow_token: Token![=>],
arg: SingleArg,
},
IfMulti {
expr: syn::Expr,
arrow_token: Token![=>],
args: MultiArg,
},
IfSingle {
expr: syn::Expr,
arrow_token: Token![=>],
arg: SingleArg,
},
Expr(SingleArg),
ForIter(ForIter),
ForIn(ForIn),
IfLet(IfLet),
If(If),
}
impl Parse for Arg {
fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
input.parse()
if input.peek(Token![for]) {
if input.peek3(Token![in]) {
input.parse().map(Self::ForIn)
} else {
input.parse().map(Self::ForIter)
}
} else if input.peek(Token![if]) {
if input.peek2(Token![let]) {
input.parse().map(Self::IfLet)
} else {
input.parse().map(Self::If)
}
} else {
input.parse().map(Self::Expr)
}
}
}
struct Pattern(syn::Pat);
impl Parse for Pattern {
fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
input.call(syn::Pat::parse_single).map(Self)
impl ToTokens for Arg {
fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) {
tokens.extend(match self {
Self::Expr(expr) => quote! { #expr },
Self::ForIter(for_iter) => quote! { #for_iter },
Self::ForIn(for_in) => quote! { #for_in },
Self::IfLet(if_let) => quote! { #if_let },
Self::If(if_) => quote! { #if_ },
});
}
}
struct ForIter {
for_token: Token![for],
pattern: Pattern,
in_token: Token![in],
iter: syn::Expr,
expr: Value,
}
impl Parse for ForIter {
fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
Ok(ForIter {
for_token: input.parse()?,
pattern: input.parse()?,
in_token: input.parse()?,
iter: input.parse()?,
_ = input.parse::<Token![for]>()?;
let expr = input.parse()?;
Ok(Self { expr })
}
}
impl ToTokens for ForIter {
fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) {
let expr = &self.expr;
tokens.extend(quote! {
for arg in #expr {
command.arg(arg);
}
});
}
}
struct ForIn {
pattern: syn::Pat,
iter: Value,
args: SingleMultiArg,
}
impl Parse for ForIn {
fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
_ = input.parse::<Token![for]>()?;
let pattern = input.call(syn::Pat::parse_single)?;
_ = input.parse::<Token![in]>()?;
let iter = input.parse()?;
_ = input.parse::<Token![=>]>()?;
let args = input.parse()?;
Ok(Self {
pattern,
iter,
args,
})
}
}
impl ToTokens for ForIn {
fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) {
let Self {
pattern,
iter,
args,
} = self;
tokens.extend(quote! {
for #pattern in #iter {
#args
}
});
}
}
struct IfLet {
if_token: Token![if],
let_token: Token![let],
pattern: Pattern,
eq_token: Token![=],
expr: syn::Expr,
pattern: syn::Pat,
expr: Value,
args: SingleMultiArg,
}
impl Parse for IfLet {
fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
Ok(IfLet {
if_token: input.parse()?,
let_token: input.parse()?,
pattern: input.parse()?,
eq_token: input.parse()?,
expr: input.parse()?,
_ = input.parse::<Token![if]>()?;
_ = input.parse::<Token![let]>()?;
let pattern = input.call(syn::Pat::parse_single)?;
_ = input.parse::<Token![=]>()?;
let expr = input.parse()?;
_ = input.parse::<Token![=>]>()?;
let args = input.parse()?;
Ok(Self {
pattern,
expr,
args,
})
}
}
struct SingleArg(syn::Expr);
impl ToTokens for IfLet {
fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) {
let Self {
pattern,
expr,
args,
} = &self;
tokens.extend(quote! {
if let #pattern = #expr {
#args
}
});
}
}
struct If {
expr: Value,
args: SingleMultiArg,
}
impl Parse for If {
fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
_ = input.parse::<Token![if]>()?;
let expr = input.parse()?;
_ = input.parse::<Token![=>]>()?;
let args = input.parse()?;
Ok(Self { expr, args })
}
}
impl ToTokens for If {
fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) {
let Self { expr, args } = self;
tokens.extend(quote! {
if #expr {
#args
}
});
}
}
enum SingleMultiArg {
Single(SingleArg),
Multi(MultiArg),
}
impl Parse for SingleMultiArg {
fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
if input.peek(token::Bracket) {
input.parse().map(Self::Multi)
} else {
input.parse().map(Self::Single)
}
}
}
impl ToTokens for SingleMultiArg {
fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) {
tokens.extend(match self {
Self::Single(arg) => quote! { #arg },
Self::Multi(args) => quote! { #args },
});
}
}
struct MultiArg(Punctuated<SingleArg, Token![,]>);
impl Parse for MultiArg {
fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
let args;
bracketed!(args in input);
Punctuated::parse_terminated(&args).map(Self)
}
}
impl ToTokens for MultiArg {
fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) {
let args = self.0.iter().collect::<Vec<_>>();
tokens.extend(quote! { #(#args)* });
}
}
struct SingleArg(Value);
impl Parse for SingleArg {
fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
@@ -130,17 +269,45 @@ impl Parse for SingleArg {
}
}
struct MultiArg {
bracket: token::Bracket,
args: Punctuated<SingleArg, Token![,]>,
impl ToTokens for SingleArg {
fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) {
let arg = &self.0;
tokens.extend(quote! {
command.arg(#arg);
});
}
}
impl Parse for MultiArg {
enum Value {
Lit(syn::Lit),
Ident(syn::Ident),
Expr(syn::Expr),
}
impl Parse for Value {
fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
let args;
Ok(MultiArg {
bracket: bracketed!(args in input),
args: input.parse_terminated(SingleArg::parse, Token![,])?,
})
if input.peek(syn::Lit) {
input.parse().map(Self::Lit)
} else if input.peek(syn::Ident) {
input.parse().map(Self::Ident)
} else {
input.parse().map(Self::Expr)
}
}
}
impl ToTokens for Value {
fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) {
tokens.extend(match self {
Self::Lit(lit) => quote! { #lit },
Self::Ident(ident) => quote! { #ident },
Self::Expr(expr) => quote! { #expr },
});
}
}
#[proc_macro]
pub fn cmd(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
let command = parse_macro_input!(input as Command);
quote! { #command }.into()
}

79
tests/cmd.rs Normal file
View File

@@ -0,0 +1,79 @@
use comlexr::cmd;
use rstest::rstest;
#[test]
fn expression() {
let command = cmd!("echo", "test");
assert_eq!(format!("{command:?}"), r#""echo" "test""#.to_string());
}
#[rstest]
#[case(false, false, r#""echo" "test""#)]
#[case(true, false, r#""echo" "test" "single""#)]
#[case(false, true, r#""echo" "test" "multi" "arg""#)]
#[case(true, true, r#""echo" "test" "single" "multi" "arg""#)]
fn if_statement(#[case] single: bool, #[case] multi: bool, #[case] expected: &str) {
let command = cmd!(
"echo",
"test",
if single => "single",
if multi => [
"multi",
"arg",
],
);
assert_eq!(format!("{command:?}"), expected.to_string());
}
#[rstest]
#[case(&[], r#""echo" "test""#)]
#[case(&["1", "2"], r#""echo" "test" "1" "2""#)]
fn for_iter(#[case] iter: &[&str], #[case] expected: &str) {
let command = cmd!(
"echo",
"test",
for iter,
);
assert_eq!(format!("{command:?}"), expected.to_string());
}
#[rstest]
#[case(&[], &[], r#""echo" "test""#)]
#[case(&["1", "2"], &[], r#""echo" "test" "1" "2""#)]
#[case(&[], &["3", "4"], r#""echo" "test" "multi" "3" "multi" "4""#)]
#[case(&["1", "2"], &["3", "4"], r#""echo" "test" "1" "2" "multi" "3" "multi" "4""#)]
fn for_in(#[case] single_iter: &[&str], #[case] multi_iter: &[&str], #[case] expected: &str) {
let command = cmd!(
"echo",
"test",
for arg in single_iter => arg,
for arg in multi_iter => [
"multi",
arg,
],
);
assert_eq!(format!("{command:?}"), expected.to_string());
}
#[rstest]
#[case(None, None, r#""echo" "test""#)]
#[case(Some("arg"), None, r#""echo" "test" "arg""#)]
#[case(None, Some("arg"), r#""echo" "test" "multi" "arg""#)]
#[case(Some("1"), Some("2"), r#""echo" "test" "1" "multi" "2""#)]
fn if_let(#[case] single: Option<&str>, #[case] multi: Option<&str>, #[case] expected: &str) {
let command = cmd!(
"echo",
"test",
if let Some(arg) = single => arg,
if let Some(arg) = multi => [
"multi",
arg,
],
);
assert_eq!(format!("{command:?}"), expected.to_string());
}