Skip to content

Commit b38592f

Browse files
committed
Initial implementation of auto_mods feature.
When enabled, if duplicate is put on a module, but that module's name is not substituted, will automatically substitute the module's name by using a suitable substitution identifier. See #7.
1 parent 3e855b7 commit b38592f

File tree

13 files changed

+208
-46
lines changed

13 files changed

+208
-46
lines changed

.travis.yml

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,15 @@ before_script:
1313
script:
1414
# Test no_features
1515
- cargo build --verbose --no-default-features
16-
- "cargo test --verbose no_features:: --no-default-features"
16+
- "cargo test --no-default-features -- --skip default_features::"
1717
# Test default_features
1818
- cargo build --verbose
1919
- "cargo test --verbose default_features::"
20+
# Test auto_mods features
21+
- cargo build --verbose --features auto_mods
22+
- "cargo test --features auto_mods-- --skip default_features::"
23+
# Test documentation code
24+
- cargo test --doc --all-features
2025
- cargo doc
2126

2227
matrix:

Cargo.toml

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,14 +18,19 @@ proc-macro = true
1818

1919
[dependencies]
2020
proc-macro-error = { version = "=1.0.3", optional = true }
21+
convert_case = { version = "=0.4.0", optional = true }
2122

2223
[dev-dependencies]
23-
macrotest = "1.0.2"
24+
macrotest = "1.0.3"
2425
doc-comment = "0.3.3"
2526

2627
[features]
27-
default = ["pretty_errors"]
28+
default = ["pretty_errors", "auto_mods"]
2829
pretty_errors = ["proc-macro-error"]
30+
auto_mods = ["convert_case"]
31+
32+
[package.metadata.docs.rs]
33+
all-features = true
2934

3035
[badges]
3136
maintenance = { status = "experimental" }

README.md

Lines changed: 19 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -11,15 +11,6 @@ This document is meant for contributors to the crate. The documentation is hoste
1111

1212
# Testing
1313

14-
Tests are divided into the following groups:
15-
16-
- [`no_features`](#anch-tests-no-features):
17-
Tests the minimal API of the crate with no features enabled.
18-
Most other tests also run these tests.
19-
20-
- [`default_features`](#anch-tests-default-features):
21-
Tests which features are defaults but does not test any functionality.
22-
2314
#### Setup
2415

2516
This crate uses [macrotest](https://crates.io/crates/macrotest) for testing expansions.
@@ -31,27 +22,38 @@ rustup toolchain install nightly
3122
rustup component add rustfmt
3223
```
3324

34-
The tests can then be run normally using `cargo test`.
35-
25+
The tests can then be run normally using `cargo test` as seen below.
3626

37-
#### <a name="anch-tests-no-features"></a>`no-features`
27+
Tests are divided into the following groups:
3828

39-
Test the basic API of the crate without any features enabled:
29+
- `no_features`:
30+
Tests the minimal API of the crate with no features enabled.
4031

4132
```
42-
cargo test no_features:: --no-default-features
33+
cargo test --no-default-features -- --skip default_features::
4334
```
4435

45-
#### <a name="anch-tests-default-features"></a>`default_features`
46-
36+
- `default_features`:
4737
Test that the correct features are enabled by default.
4838
This is to ensure a change doesn't change which features are on by default.
49-
However, this does not test the features themselves:
39+
However, this does not test the features themselves.
5040

5141
```
5242
cargo test default_features::
5343
```
5444

45+
- `features`:
46+
Tests any combination of features. After `--features` add a comma separated list of features to test:
47+
48+
```
49+
cargo test --features auto_mods,pretty_errors -- --skip default_features::
50+
```
51+
52+
- `documentation`:
53+
Tests code in the documentation. Even though some of the other test groups might test some of the documentaion code, they are not guaranteed to run all tests. E.g. the test of the cargo readme file (`cargo-readme.md` are only run when this command is used.
54+
```
55+
cargo test --doc --all-features
56+
```
5557
# Formatting
5658

5759
We use `rustfmt` to manage the formatting of this crate's code.

src/auto_mods.rs

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
use crate::substitute::Substitution;
2+
use convert_case::{Case, Casing};
3+
use proc_macro::{Ident, TokenStream, TokenTree};
4+
use std::collections::HashMap;
5+
6+
/// If the given item is a module declaration and the substitutions don't
7+
/// reassign the module identifier for each substitution, this function
8+
/// will try to do so.
9+
pub fn unambiguate_module(module: Ident, substitutions: &mut Vec<HashMap<String, Substitution>>)
10+
{
11+
let ident = find_simple(substitutions).unwrap();
12+
// All match
13+
for group in substitutions.iter_mut()
14+
{
15+
let postfix = group[&ident]
16+
.substitutes_identifier()
17+
.unwrap()
18+
.to_string()
19+
.to_case(Case::Snake);
20+
let replacement_name = module.to_string() + "_" + &postfix;
21+
let replacement = Ident::new(&replacement_name, module.span());
22+
group.insert(
23+
module.to_string(),
24+
Substitution::new_simple(TokenStream::from(TokenTree::Ident(replacement))),
25+
);
26+
}
27+
}
28+
29+
fn find_simple(substitutions: &mut Vec<HashMap<String, Substitution>>) -> Option<String>
30+
{
31+
'outer: for ident in substitutions[0].keys()
32+
{
33+
for group in substitutions.iter()
34+
{
35+
if group[ident].substitutes_identifier().is_none()
36+
{
37+
continue 'outer;
38+
}
39+
}
40+
return Some(ident.clone());
41+
}
42+
None
43+
}

src/crate_readme_test.rs

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
#![cfg(doctest)]
2-
use doc_comment::doctest;
3-
4-
// Tests the crate readme file's Rust examples.
5-
doctest!("../cargo-readme.md");
1+
#[cfg(doctest)]
2+
// Tests the crate readme-file's Rust examples.
3+
doc_comment::doctest!("../cargo-readme.md");

src/lib.rs

Lines changed: 66 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -501,17 +501,22 @@
501501
//! As an example, libraries that have two or more structs/traits with similar
502502
//! APIs might use this macro to test them without having to copy-paste test
503503
//! cases and manually make the needed edits.
504-
use proc_macro::{Span, TokenStream};
505-
#[cfg(feature = "pretty_errors")]
506-
use proc_macro_error::{abort, proc_macro_error};
507504
508505
mod parse;
509506
mod parse_utils;
510507
mod substitute;
511508
// Tests the crate readme file's Rust examples.
509+
#[cfg(feature = "auto_mods")]
510+
mod auto_mods;
512511
mod crate_readme_test;
513512

513+
use crate::parse_utils::{next_token, parse_group};
514+
#[cfg(feature = "auto_mods")]
515+
use auto_mods::*;
514516
use parse::*;
517+
use proc_macro::{Ident, Span, TokenStream, TokenTree};
518+
#[cfg(feature = "pretty_errors")]
519+
use proc_macro_error::{abort, proc_macro_error};
515520
use substitute::*;
516521

517522
/// Duplicates the item it is applied to and substitutes specific identifiers
@@ -741,7 +746,7 @@ pub fn duplicate(attr: TokenStream, item: TokenStream) -> TokenStream
741746
match duplicate_impl(attr, item)
742747
{
743748
Ok(result) => result,
744-
Err(err) => abort(err.0, err.1),
749+
Err(err) => abort(err.0, &err.1),
745750
}
746751
}
747752

@@ -750,18 +755,68 @@ pub fn duplicate(attr: TokenStream, item: TokenStream) -> TokenStream
750755
/// `allow_short`: If true, accepts short syntax
751756
fn duplicate_impl(attr: TokenStream, item: TokenStream) -> Result<TokenStream, (Span, String)>
752757
{
753-
let subs = parse_attr(attr, Span::call_site())?;
758+
#[allow(unused_mut)]
759+
let mut subs = parse_attr(attr, Span::call_site())?;
760+
761+
if let Some(module) = get_module_name(&item)
762+
{
763+
if !subs[0].contains_key(&module.to_string())
764+
{
765+
#[cfg(not(feature = "auto_mods"))]
766+
{
767+
abort(
768+
module.span(),
769+
"Duplicating the module '{}' without giving each duplicate a unique \
770+
name.\nHint: Enable the 'duplicate' crate's '{}' feature to automatically \
771+
generate unique module names",
772+
);
773+
}
774+
#[cfg(feature = "auto_mods")]
775+
{
776+
unambiguate_module(module, &mut subs);
777+
}
778+
}
779+
}
754780
let result = substitute(item, subs);
755781
Ok(result)
756782
}
757783

758-
#[cfg(feature = "pretty_errors")]
759-
fn abort(span: Span, msg: String) -> !
784+
/// Terminates with an error and produces the given message.
785+
///
786+
/// The `pretty_errors` feature can be enabled, the span is shown
787+
/// with the error message.
788+
fn abort(#[allow(unused_variables)] span: Span, msg: &str) -> !
760789
{
761-
abort!(span, msg)
790+
#[cfg(feature = "pretty_errors")]
791+
{
792+
abort!(span, msg);
793+
}
794+
#[cfg(not(feature = "pretty_errors"))]
795+
{
796+
panic!(format!("{}", msg));
797+
}
762798
}
763-
#[cfg(not(feature = "pretty_errors"))]
764-
fn abort(_: Span, msg: String) -> !
799+
800+
/// Extract the name of the module assuming the given item is a module
801+
/// declaration.
802+
///
803+
/// If not, returns None.
804+
fn get_module_name(item: &TokenStream) -> Option<Ident>
765805
{
766-
panic!(msg);
806+
let mut iter = item.clone().into_iter();
807+
808+
if let TokenTree::Ident(mod_keyword) = next_token(&mut iter, "").unwrap_or(None)?
809+
{
810+
if mod_keyword.to_string() == "mod"
811+
{
812+
if let TokenTree::Ident(module) = next_token(&mut iter, "").unwrap_or(None)?
813+
{
814+
if parse_group(&mut iter, Span::call_site(), "").is_ok()
815+
{
816+
return Some(module);
817+
}
818+
}
819+
}
820+
}
821+
None
767822
}

src/substitute.rs

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,25 @@ impl Substitution
146146
Err(())
147147
}
148148
}
149+
150+
#[cfg(feature = "auto_mods")]
151+
/// If this substitution simply produces an identifier and nothing else,
152+
/// then that identifier is returned, otherwise None
153+
pub fn substitutes_identifier(&self) -> Option<Ident>
154+
{
155+
if self.sub.len() == 1
156+
{
157+
if let SubType::Token(token) = &self.sub[0]
158+
{
159+
let mut iter = token.clone().into_iter();
160+
if let TokenTree::Ident(ident) = iter.next()?
161+
{
162+
return Some(ident);
163+
}
164+
}
165+
}
166+
None
167+
}
149168
}
150169

151170
/// Duplicates the given token stream, substituting any identifiers found.

tests/auto_mods/.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
/testing
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
use duplicate::duplicate;
2+
mod module_some_name_1 {
3+
pub struct SomeName1();
4+
}
5+
mod module_some_name_2 {
6+
pub struct SomeName2();
7+
}
8+
mod module_some_name_3 {
9+
pub struct SomeName3();
10+
}

tests/auto_mods/from/short_syntax.rs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
use duplicate::duplicate;
2+
3+
#[duplicate(
4+
name;
5+
[SomeName1];
6+
[SomeName2];
7+
[SomeName3]
8+
)]
9+
mod module {
10+
pub struct name();
11+
}

tests/auto_mods/mod.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
#[test]
2+
pub fn test_expansions()
3+
{
4+
use crate::utils::ExpansionTester;
5+
let mut test = ExpansionTester::new("tests/auto_mods", "testing");
6+
test.add_source_dir("from", ExpansionTester::copy());
7+
test.add_source_dir("expected", ExpansionTester::copy());
8+
test.execute_tests();
9+
}

tests/default_features/mod.rs

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,27 @@
11
//! This file tests which features are on by default
22
//!
3-
//! To tests this correctly, tests should be run without using the '--features'
4-
//! flag to ensure all default features are enabled and no other.
3+
//! To tests this correctly, tests should be run without using the `--features`
4+
//! and `--no-default-features` flags to ensure that only the default features
5+
//! are enable.
56
67
/// Tests that a feature is enabled by default.
78
///
89
/// Must first be given a unique identifier for the feature (which doesn't
9-
/// necessarily need to be the same as the feature name), and then a string
10-
/// containing the feature name.
10+
/// necessarily need to be the same as the feature name, but it might be a good
11+
/// idea), and then a string containing the feature name.
1112
///
1213
/// ### Example
1314
///
14-
/// The following code will test whether a feature name "feature_name" is
15+
/// The following code will test whether a feature named "feature_name" is
1516
/// enabled by default.
1617
///
1718
/// ```
1819
/// default_feature!{feature_id "feature_name"}
1920
/// ```
2021
macro_rules! default_features {
21-
( $feature:ident $feature_string:literal
22-
// $($feature_rest:ident $feature_string_rest:literal)*
23-
) => {
22+
{
23+
$feature:ident $feature_string:literal
24+
} => {
2425
mod $feature
2526
{
2627
#[cfg(feature = $feature_string)]
@@ -29,12 +30,13 @@ macro_rules! default_features {
2930
const IS_DEFAULT: bool = false;
3031

3132
#[test]
32-
pub fn test_is_default()
33+
pub fn is_default()
3334
{
34-
assert!(IS_DEFAULT, "Feature is not enabled by default.");
35+
assert!(IS_DEFAULT, format!("Feature '{}' is not enabled by default.", $feature_string));
3536
}
3637
}
3738
};
3839
}
3940

4041
default_features!(pretty_errors "pretty_errors");
42+
default_features!(auto_mods "auto_mods");

tests/tests.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
#[cfg(feature = "auto_mods")]
2+
mod auto_mods;
13
mod default_features;
24
mod no_features;
35
mod utils;

0 commit comments

Comments
 (0)