Skip to content

Commit

Permalink
Implemented 'substitute!' and 'substitute_item'.
Browse files Browse the repository at this point in the history
These macro allow only global substitutions.
  • Loading branch information
Emoun committed Apr 23, 2024
1 parent 47d25fe commit 64d88ee
Show file tree
Hide file tree
Showing 20 changed files with 259 additions and 19 deletions.
121 changes: 113 additions & 8 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -1017,6 +1017,48 @@ pub fn duplicate_item(attr: TokenStream, item: TokenStream) -> TokenStream
}
}

/// Substitutes specific identifiers for different code
/// snippets.
///
/// ```
/// # struct Some<T1,T2>(T1,T2);
/// # struct Complex<T>(T);
/// # struct Type<T>(T);
/// # struct WeDont<T1,T2,T3>(T1,T2,T3);
/// # struct Want();
/// # struct To();
/// # struct Repeat();
/// # struct Other();
/// # use duplicate::substitute_item;
/// #[substitute_item(
/// typ1 [Some<Complex<()>, Type<WeDont<Want, To, Repeat>>>];
/// typ2 [Some<Other, Complex<Type<(To, Repeat)>>>];
/// )]
/// 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.
///
Expand Down Expand Up @@ -1094,17 +1136,78 @@ 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>(T1,T2);
/// # struct Complex<T>(T);
/// # struct Type<T>(T);
/// # struct WeDont<T1,T2,T3>(T1,T2,T3);
/// # struct Want();
/// # struct To();
/// # struct Repeat();
/// # struct Other();
/// # use duplicate::substitute;
///
/// substitute!{
/// [
/// typ1 [Some<Complex<()>, Type<WeDont<Want, To, Repeat>>>];
/// typ2 [Some<Other, Complex<Type<(To, Repeat)>>>];
/// ]
/// 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<T> = std::result::Result<T, Error>;

/// 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>,
) -> 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))
{
Ok((invocation, _)) =>
{
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: [...]")),
};
Expand All @@ -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<T> = std::result::Result<T, Error>;

/// Implements the macro.
/// Implements the duplicate macros.
fn duplicate_impl(attr: TokenStream, item: TokenStream) -> Result<TokenStream>
{
let dup_def = parse_invocation(attr)?;
Expand All @@ -1132,6 +1231,12 @@ fn duplicate_impl(attr: TokenStream, item: TokenStream) -> Result<TokenStream>
)
}

/// Implements the substitute macros
fn substitute_impl(attr: TokenStream, item: TokenStream) -> Result<TokenStream>
{
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
Expand Down
47 changes: 47 additions & 0 deletions src/parse.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<SubstitutionGroup>
{
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.
///
Expand Down
1 change: 1 addition & 0 deletions tests/errors/basic/substitute_item_with_duplicate
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Unexpected token.
1 change: 1 addition & 0 deletions tests/errors/basic/substitute_item_with_duplicate_2
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Expected '(' or '['.
1 change: 1 addition & 0 deletions tests/errors/basic/substitute_item_with_duplicate_3
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Unexpected delimiter.
1 change: 1 addition & 0 deletions tests/errors/basic/substitute_item_with_duplicate_4
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Unexpected end of code.
2 changes: 2 additions & 0 deletions tests/errors/highlight/substitute_item_with_duplicate
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
6 | 123
| ^^^
2 changes: 2 additions & 0 deletions tests/errors/highlight/substitute_item_with_duplicate_2
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
6 | dup_sub;
| ^
4 changes: 4 additions & 0 deletions tests/errors/highlight/substitute_item_with_duplicate_3
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
6 | / [
7 | | name2 [sub2]
8 | | ]
| |_^
1 change: 1 addition & 0 deletions tests/errors/hint/substitute_item_with_duplicate
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Hint: Expected a substitution identifier.
1 change: 1 addition & 0 deletions tests/errors/hint/substitute_item_with_duplicate_2
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Hint: Only global substitutions are allowed. Try 'duplicate' or 'duplicate_item'.
1 change: 1 addition & 0 deletions tests/errors/hint/substitute_item_with_duplicate_3
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Hint: Only global substitutions are allowed. Try 'duplicate' or 'duplicate_item'.
1 change: 1 addition & 0 deletions tests/errors/hint/substitute_item_with_duplicate_4
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Hint: Expected a substitution identifier.
9 changes: 9 additions & 0 deletions tests/errors/source/substitute_item_with_duplicate.rs
Original file line number Diff line number Diff line change
@@ -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
11 changes: 11 additions & 0 deletions tests/errors/source/substitute_item_with_duplicate_2.rs
Original file line number Diff line number Diff line change
@@ -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
14 changes: 14 additions & 0 deletions tests/errors/source/substitute_item_with_duplicate_3.rs
Original file line number Diff line number Diff line change
@@ -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
6 changes: 6 additions & 0 deletions tests/errors/source/substitute_item_with_duplicate_4.rs
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
use duplicate::*;
pub struct SomeStruct(i16);
pub struct SomeStruct2(i32);
pub struct SomeStruct3(&'static i64);
pub struct SomeStruct3(&'static i64);
pub struct SomeStruct4(u16);
pub struct SomeStruct5(u32);
pub struct SomeStruct6(&'static u64);
26 changes: 23 additions & 3 deletions tests/no_features/from/only_global_substitutions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
23 changes: 16 additions & 7 deletions tests/utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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`):
Expand Down Expand Up @@ -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();
Expand Down

0 comments on commit 64d88ee

Please sign in to comment.