diff --git a/src/lib.rs b/src/lib.rs index 7e4fa5a..f8ae303 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -722,7 +722,7 @@ use parse::*; use proc_macro::{Delimiter, Group, Ident, Span, TokenStream}; #[cfg(feature = "pretty_errors")] use proc_macro_error::{abort, proc_macro_error}; -use std::collections::HashMap; +use std::{collections::HashMap, iter::empty}; use substitute::*; /// Duplicates the item and substitutes specific identifiers for different code @@ -1017,6 +1017,48 @@ pub fn duplicate_item(attr: TokenStream, item: TokenStream) -> TokenStream } } +/// Substitutes specific identifiers for different code +/// snippets. +/// +/// ``` +/// # struct Some(T1,T2); +/// # struct Complex(T); +/// # struct Type(T); +/// # struct WeDont(T1,T2,T3); +/// # struct Want(); +/// # struct To(); +/// # struct Repeat(); +/// # struct Other(); +/// # use duplicate::substitute_item; +/// #[substitute_item( +/// typ1 [Some, Type>>]; +/// typ2 [Some>>]; +/// )] +/// fn method( +/// arg1: typ1, +/// arg2: typ2) +/// -> (typ1, typ2) +/// { +/// # /* +/// ... +/// # */ +/// # unimplemented!() +/// } +/// ``` +/// +/// The global substitutions (`typ1` and `typ2`) are substituted in both +/// their occurrences. Global substitutions are `;` separated. +#[proc_macro_attribute] +#[cfg_attr(feature = "pretty_errors", proc_macro_error)] +pub fn substitute_item(attr: TokenStream, item: TokenStream) -> TokenStream +{ + match substitute_impl(attr, item) + { + Ok(result) => result, + Err(err) => abort(err), + } +} + /// Duplicates the given code and substitutes specific identifiers /// for different code snippets in each duplicate. /// @@ -1094,9 +1136,70 @@ pub fn duplicate_item(attr: TokenStream, item: TokenStream) -> TokenStream #[proc_macro] #[cfg_attr(feature = "pretty_errors", proc_macro_error)] pub fn duplicate(stream: TokenStream) -> TokenStream +{ + inline_macro_impl(stream, duplicate_impl) +} + +/// Substitutes specific identifiers for different code +/// snippets. +/// +/// This is a function-like procedural macro version of [`substitute_item`]. +/// It's functionality is the exact same. The only difference is that +/// `substitute` doesn't only substitute the following item, but all code given +/// to it after the invocation block. ``` +/// # struct Some(T1,T2); +/// # struct Complex(T); +/// # struct Type(T); +/// # struct WeDont(T1,T2,T3); +/// # struct Want(); +/// # struct To(); +/// # struct Repeat(); +/// # struct Other(); +/// # use duplicate::substitute; +/// +/// substitute!{ +/// [ +/// typ1 [Some, Type>>]; +/// typ2 [Some>>]; +/// ] +/// fn method( +/// arg1: typ1, +/// arg2: typ2) +/// -> (typ1, typ2) +/// { +/// # /* +/// ... +/// # */ +/// # unimplemented!() +/// } +/// } +/// ``` +/// +/// The global substitutions (`typ1` and `typ2`) are substituted in both +/// their occurrences. Global substitutions are `;` separated. +#[proc_macro] +#[cfg_attr(feature = "pretty_errors", proc_macro_error)] +pub fn substitute(stream: TokenStream) -> TokenStream +{ + inline_macro_impl(stream, substitute_impl) +} + +/// A result that specified where in the token stream the error occured +/// and is accompanied by a message. +type Result = std::result::Result; + +/// Parses an inline macro invocation where the invocation syntax is within +/// initial brackets. +/// +/// Extracts the invocation syntax and body to be duplicated/substituted +/// and passes them to the given function. +fn inline_macro_impl( + stream: TokenStream, + f: fn(TokenStream, TokenStream) -> Result, +) -> TokenStream { let empty_globals = SubstitutionGroup::new(); - let mut iter = TokenIter::new(stream, &empty_globals, std::iter::empty()); + let mut iter = TokenIter::new(stream, &empty_globals, empty()); let result = match iter.next_group(Some(Delimiter::Bracket)) { @@ -1104,7 +1207,7 @@ pub fn duplicate(stream: TokenStream) -> TokenStream { let invocation_body = invocation.to_token_stream(); - duplicate_impl(invocation_body, iter.to_token_stream()) + f(invocation_body, iter.to_token_stream()) }, Err(err) => Err(err.hint("Expected invocation within brackets: [...]")), }; @@ -1116,11 +1219,7 @@ pub fn duplicate(stream: TokenStream) -> TokenStream } } -/// A result that specified where in the token stream the error occured -/// and is accompanied by a message. -type Result = std::result::Result; - -/// Implements the macro. +/// Implements the duplicate macros. fn duplicate_impl(attr: TokenStream, item: TokenStream) -> Result { let dup_def = parse_invocation(attr)?; @@ -1132,6 +1231,12 @@ fn duplicate_impl(attr: TokenStream, item: TokenStream) -> Result ) } +/// Implements the substitute macros +fn substitute_impl(attr: TokenStream, item: TokenStream) -> Result +{ + duplicate_and_substitute(item, &parse_global_substitutions_only(attr)?, empty()) +} + /// Terminates with an error and produces the given message. /// /// The `pretty_errors` feature can be enabled, the span is shown diff --git a/src/parse.rs b/src/parse.rs index 0ad0503..0a2832f 100644 --- a/src/parse.rs +++ b/src/parse.rs @@ -11,6 +11,53 @@ use crate::{ use proc_macro::{Delimiter, Ident, Span, TokenStream, TokenTree}; use std::collections::HashSet; +/// Parses all global substitutions, returning them. +/// +/// If there are other tokens than global substitutions, returns an error. +pub(crate) fn parse_global_substitutions_only(attr: TokenStream) -> Result +{ + let empty_global = SubstitutionGroup::new(); + let mut iter = TokenIter::new(attr, &empty_global, std::iter::empty()); + let global_substitutions = validate_global_substitutions(&mut iter)?; + + if let Ok(None) = iter.peek() + { + // Accept global substitutions on their own + if global_substitutions.substitutions.is_empty() + { + // There are no global substitutions, return error requiring it + Err(iter + .extract_identifier(Some("a substitution identifier")) + .unwrap_err()) + } + else + { + Ok(global_substitutions) + } + } + else + { + // There are more tokens, just try to get another substitution and return its + // error + let mut err = extract_inline_substitution(&mut iter).unwrap_err(); + + #[cfg(feature = "pretty_errors")] + { + if validate_short_get_identifiers(&mut iter.clone()).is_ok() + || validate_verbose_invocation(&mut iter) + .map(|res| res.is_some()) + .unwrap_or(false) + { + err = err.hint( + "Hint: Only global substitutions are allowed. Try 'duplicate' or \ + 'duplicate_item'.", + ); + } + } + Err(err) + } +} + /// Parses the invocation of duplicate, returning all the substitutions that /// should be made to code. /// diff --git a/tests/errors/basic/substitute_item_with_duplicate b/tests/errors/basic/substitute_item_with_duplicate new file mode 100644 index 0000000..88d2729 --- /dev/null +++ b/tests/errors/basic/substitute_item_with_duplicate @@ -0,0 +1 @@ +Unexpected token. \ No newline at end of file diff --git a/tests/errors/basic/substitute_item_with_duplicate_2 b/tests/errors/basic/substitute_item_with_duplicate_2 new file mode 100644 index 0000000..4915221 --- /dev/null +++ b/tests/errors/basic/substitute_item_with_duplicate_2 @@ -0,0 +1 @@ +Expected '(' or '['. \ No newline at end of file diff --git a/tests/errors/basic/substitute_item_with_duplicate_3 b/tests/errors/basic/substitute_item_with_duplicate_3 new file mode 100644 index 0000000..97dfb1a --- /dev/null +++ b/tests/errors/basic/substitute_item_with_duplicate_3 @@ -0,0 +1 @@ +Unexpected delimiter. \ No newline at end of file diff --git a/tests/errors/basic/substitute_item_with_duplicate_4 b/tests/errors/basic/substitute_item_with_duplicate_4 new file mode 100644 index 0000000..72c522a --- /dev/null +++ b/tests/errors/basic/substitute_item_with_duplicate_4 @@ -0,0 +1 @@ +Unexpected end of code. \ No newline at end of file diff --git a/tests/errors/highlight/substitute_item_with_duplicate b/tests/errors/highlight/substitute_item_with_duplicate new file mode 100644 index 0000000..20bac65 --- /dev/null +++ b/tests/errors/highlight/substitute_item_with_duplicate @@ -0,0 +1,2 @@ +6 | 123 + | ^^^ \ No newline at end of file diff --git a/tests/errors/highlight/substitute_item_with_duplicate_2 b/tests/errors/highlight/substitute_item_with_duplicate_2 new file mode 100644 index 0000000..388d77f --- /dev/null +++ b/tests/errors/highlight/substitute_item_with_duplicate_2 @@ -0,0 +1,2 @@ +6 | dup_sub; + | ^ \ No newline at end of file diff --git a/tests/errors/highlight/substitute_item_with_duplicate_3 b/tests/errors/highlight/substitute_item_with_duplicate_3 new file mode 100644 index 0000000..e2ce018 --- /dev/null +++ b/tests/errors/highlight/substitute_item_with_duplicate_3 @@ -0,0 +1,4 @@ +6 | / [ +7 | | name2 [sub2] +8 | | ] + | |_^ \ No newline at end of file diff --git a/tests/errors/hint/substitute_item_with_duplicate b/tests/errors/hint/substitute_item_with_duplicate new file mode 100644 index 0000000..83e86a9 --- /dev/null +++ b/tests/errors/hint/substitute_item_with_duplicate @@ -0,0 +1 @@ + Hint: Expected a substitution identifier. \ No newline at end of file diff --git a/tests/errors/hint/substitute_item_with_duplicate_2 b/tests/errors/hint/substitute_item_with_duplicate_2 new file mode 100644 index 0000000..eb752c9 --- /dev/null +++ b/tests/errors/hint/substitute_item_with_duplicate_2 @@ -0,0 +1 @@ + Hint: Only global substitutions are allowed. Try 'duplicate' or 'duplicate_item'. \ No newline at end of file diff --git a/tests/errors/hint/substitute_item_with_duplicate_3 b/tests/errors/hint/substitute_item_with_duplicate_3 new file mode 100644 index 0000000..eb752c9 --- /dev/null +++ b/tests/errors/hint/substitute_item_with_duplicate_3 @@ -0,0 +1 @@ + Hint: Only global substitutions are allowed. Try 'duplicate' or 'duplicate_item'. \ No newline at end of file diff --git a/tests/errors/hint/substitute_item_with_duplicate_4 b/tests/errors/hint/substitute_item_with_duplicate_4 new file mode 100644 index 0000000..83e86a9 --- /dev/null +++ b/tests/errors/hint/substitute_item_with_duplicate_4 @@ -0,0 +1 @@ + Hint: Expected a substitution identifier. \ No newline at end of file diff --git a/tests/errors/source/substitute_item_with_duplicate.rs b/tests/errors/source/substitute_item_with_duplicate.rs new file mode 100644 index 0000000..5340051 --- /dev/null +++ b/tests/errors/source/substitute_item_with_duplicate.rs @@ -0,0 +1,9 @@ +use duplicate::*; +// Test that substitute macros look for inline substitutions +#[substitute_item( + name [sub1]; + ty [u32]; + 123 +)]//duplicate_end +pub struct name(ty); +//item_end diff --git a/tests/errors/source/substitute_item_with_duplicate_2.rs b/tests/errors/source/substitute_item_with_duplicate_2.rs new file mode 100644 index 0000000..598510e --- /dev/null +++ b/tests/errors/source/substitute_item_with_duplicate_2.rs @@ -0,0 +1,11 @@ +use duplicate::*; +// Tests that if the globals are followed by short syntax, the hint refers to 'duplicate' +#[substitute_item( + name [sub1]; + ty [u32]; + dup_sub; + [i8]; + [i16]; +)]//duplicate_end +pub struct name(ty); +//item_end diff --git a/tests/errors/source/substitute_item_with_duplicate_3.rs b/tests/errors/source/substitute_item_with_duplicate_3.rs new file mode 100644 index 0000000..1c7ce62 --- /dev/null +++ b/tests/errors/source/substitute_item_with_duplicate_3.rs @@ -0,0 +1,14 @@ +use duplicate::*; +// Tests that if the globals are followed by verbose syntax, the hint refers to 'duplicate' +#[substitute_item( + name [sub1]; + ty [u32]; + [ + name2 [sub2] + ] + [ + name2 [sub3] + ] +)]//duplicate_end +pub struct name(ty); +//item_end diff --git a/tests/errors/source/substitute_item_with_duplicate_4.rs b/tests/errors/source/substitute_item_with_duplicate_4.rs new file mode 100644 index 0000000..00e8406 --- /dev/null +++ b/tests/errors/source/substitute_item_with_duplicate_4.rs @@ -0,0 +1,6 @@ +use duplicate::*; +// Test that substitute macros require at least one global substitution +#[substitute_item( +)]//duplicate_end +pub struct name(ty); +//item_end diff --git a/tests/no_features/expected/only_global_substitutions.expanded.rs b/tests/no_features/expected/only_global_substitutions.expanded.rs index 5b06184..f099c5c 100644 --- a/tests/no_features/expected/only_global_substitutions.expanded.rs +++ b/tests/no_features/expected/only_global_substitutions.expanded.rs @@ -1,4 +1,7 @@ use duplicate::*; pub struct SomeStruct(i16); pub struct SomeStruct2(i32); -pub struct SomeStruct3(&'static i64); \ No newline at end of file +pub struct SomeStruct3(&'static i64); +pub struct SomeStruct4(u16); +pub struct SomeStruct5(u32); +pub struct SomeStruct6(&'static u64); \ No newline at end of file diff --git a/tests/no_features/from/only_global_substitutions.rs b/tests/no_features/from/only_global_substitutions.rs index 54032b1..522b2ca 100644 --- a/tests/no_features/from/only_global_substitutions.rs +++ b/tests/no_features/from/only_global_substitutions.rs @@ -4,11 +4,11 @@ use duplicate::*; ty [i16]; )]//duplicate_end pub struct SomeStruct(ty); - //item_end + #[duplicate_item( -Name [SomeStruct2]; -ty [i32]; + Name [SomeStruct2]; + ty [i32]; )]//duplicate_end pub struct Name(ty); //item_end @@ -19,3 +19,23 @@ pub struct Name(ty); )]//duplicate_end pub struct Name(ty([&'static i64])); //item_end + +#[substitute_item( + ty [u16]; +)]//duplicate_end +pub struct SomeStruct4(ty); +//item_end + +#[substitute_item( + Name [SomeStruct5]; + ty [u32]; +)]//duplicate_end +pub struct Name(ty); +//item_end + +#[substitute_item( + Name [SomeStruct6]; + ty(extra) [extra]; +)]//duplicate_end +pub struct Name(ty([&'static u64])); +//item_end \ No newline at end of file diff --git a/tests/utils.rs b/tests/utils.rs index 2dca4de..3f3a587 100644 --- a/tests/utils.rs +++ b/tests/utils.rs @@ -195,8 +195,8 @@ impl<'a> ExpansionTester<'a> /// Generates an action that creates two versions of the given file in the /// testing directory. The source file must use the attribute /// macro, where: - /// - The invocation must starts with `#[duplicate_item(` on a the first - /// line + /// - The invocation must starts with `#[duplicate_item(` or + /// `#[substitute_item(` on a the first line /// (with nothing else). Notice that you must not import the attribute but /// use its full path. /// - Then the body of the invocation. Both syntaxes are allowed. @@ -209,11 +209,11 @@ impl<'a> ExpansionTester<'a> /// /// This action will then generate 2 versions of this file. The first is /// almost identical the original, but the second will change the invocation - /// to instead use `duplicate`. It uses the exact rules specified - /// above to correctly change the code, so any small deviation from the - /// above rules might result in an error. The name of the first version is - /// the same as the original and the second version is prefixed with - /// 'inline_' + /// to instead use `duplicate` or `substitute`. It uses the exact rules + /// specified above to correctly change the code, so any small deviation + /// from the above rules might result in an error. The name of the first + /// version is the same as the original and the second version is prefixed + /// with 'inline_' /// /// ### Example /// Original file (`test.rs`): @@ -284,6 +284,15 @@ impl<'a> ExpansionTester<'a> .write_all("duplicate!{[".as_bytes()) .unwrap(); }, + "#[substitute_item(" => + { + dest_file + .write_all("#[substitute_item(".as_bytes()) + .unwrap(); + dest_inline_file + .write_all("substitute!{[".as_bytes()) + .unwrap(); + }, ")]//duplicate_end" => { dest_file.write_all(")]".as_bytes()).unwrap();