From 798a1018aa42cd1d54d426f542859bb50ec83012 Mon Sep 17 00:00:00 2001 From: Colin Kiegel Date: Sun, 5 Mar 2017 14:16:52 +0100 Subject: [PATCH] restricted computation in implementation - implement #10 - restrict to array/tuple/struct access and function calls --- .travis.yml | 10 +- Cargo.toml | 4 + src/lib.rs | 145 +++++++++++++++++++++++++++- tests/compile-fail/deny-closures.rs | 9 ++ tests/compile-fail/deny-macros.rs | 9 ++ tests/compile-fail/deny-numbers.rs | 9 ++ tests/compile-fail/deny-plus.rs | 9 ++ tests/compiletests.rs | 43 +++++++++ tests/computation.rs | 134 +++++++++++++++++++++++++ 9 files changed, 368 insertions(+), 4 deletions(-) create mode 100644 tests/compile-fail/deny-closures.rs create mode 100644 tests/compile-fail/deny-macros.rs create mode 100644 tests/compile-fail/deny-numbers.rs create mode 100644 tests/compile-fail/deny-plus.rs create mode 100644 tests/compiletests.rs create mode 100644 tests/computation.rs diff --git a/.travis.yml b/.travis.yml index 882ddd1..b576dcd 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,11 +3,17 @@ sudo: false language: rust rust: - - nightly - stable - 1.12.1 - 1.13.0 - beta +matrix: + include: + - rust: nightly + env: CARGO_FEATURES=dev_nightly # enable compiletests +env: + global: + - CARGO_FEATURES="" script: - - cargo test + - cargo test --features "$CARGO_FEATURES" diff --git a/Cargo.toml b/Cargo.toml index a17fc81..bbbfffa 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,3 +8,7 @@ repository = "https://github.com/dtolnay/quote" documentation = "https://docs.rs/quote/" keywords = ["syn"] include = ["Cargo.toml", "src/**/*.rs", "tests/**/*.rs", "README.md", "LICENSE-APACHE", "LICENSE-MIT"] +[features] +dev_nightly = ["compiletest_rs"] +[dependencies] +compiletest_rs = { version = "0.2", optional = true } diff --git a/src/lib.rs b/src/lib.rs index 4bc4188..336f063 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -50,6 +50,24 @@ //! return quote! { /* ... */ #t /* ... */ }; //! ``` //! +//! You can use `#{...}` for some basic computations inside of quotations. Computations are +//! restricted to combinations of: +//! +//! - `#{x[0]}` - index arrays with integers 0..32 +//! - `#{x.0}` - index tuple structs with integers 0..32 +//! - `#{x.foo}` - access fields +//! - `#{x()}` - call functions (without arguments) +//! +//! Note: +//! - Any chained combination of the above is also possible, like `#{self.foo[0].bar()}`. +//! But please consider replacing too complex computations with helper variables `#bar` +//! in order to improve the readability for other people - thank you in advance. ;-) +//! - These computations can be particularly useful if you want to implement `quote::ToTokens` +//! for a custom struct. You can then reference the struct fields directly via `#{self.foo}` +//! etc. +//! - computations `#{...}` _inside_ of repetitions `#(...)*` are treated as constant expressions +//! and are not iterated over - in contrast to any other `#x` inside of a repetition. +//! //! Call `to_string()` or `as_str()` on a Tokens to get a `String` or `&str` of Rust //! code. //! @@ -98,8 +116,8 @@ macro_rules! pounded_var_names { pounded_var_names!($finish ($($found)*) $($inner)* $($rest)*) }; - ($finish:ident ($($found:ident)*) # { $($inner:tt)* } $($rest:tt)*) => { - pounded_var_names!($finish ($($found)*) $($inner)* $($rest)*) + ($finish:ident ($($found:ident)*) # { $($ignore:tt)* } $($rest:tt)*) => { + pounded_var_names!($finish ($($found)*) $($rest)*) }; ($finish:ident ($($found:ident)*) # $first:ident $($rest:tt)*) => { @@ -181,6 +199,121 @@ macro_rules! multi_zip_expr { }; } +// in: validate_computation_in_interpolation!(@COMPUTATION self.foo()) +// result: OK - empty token tree +// +// in: validate_computation_in_interpolation!(@COMPUTATION self.x.map(|x| 3*x)) +// result: Parsing error "unexpected Token `|`" +#[macro_export] +#[doc(hidden)] +macro_rules! validate_computation_in_interpolation { + // input must either start with `($ident...)` + // + // -> in order to support the computation `#{(matrix.2).3}` + (@ENTRY ($ident:ident $($branch:tt)*) $($rest:tt)*) => { + validate_computation_in_interpolation!(@AFTER_IDENT $($branch)*); + validate_computation_in_interpolation!(@AFTER_IDENT $($rest)*); + }; + + // .. or it must start with an identifier `$ident` + (@ENTRY $ident:ident $($rest:tt)*) => { + validate_computation_in_interpolation!(@AFTER_IDENT $($rest)*); + }; + + // done when empty + (@AFTER_IDENT) => {}; + + // function calls are ok - but only without arguments + (@AFTER_IDENT () $($rest:tt)*) => { + validate_computation_in_interpolation!(@AFTER_IDENT $($rest)*); + }; + + // struct access is ok - both by field ident or tuple index + (@AFTER_IDENT . $expect_number_or_ident:tt $($rest:tt)*) => { + validate_computation_in_interpolation!(@IS [NUMBER IDENT] $expect_number_or_ident); + validate_computation_in_interpolation!(@AFTER_IDENT $($rest)*); + }; + + // array access is ok - but only with for indices 0..32 + (@AFTER_IDENT [$($expect_number:tt)*] $($rest:tt)*) => { + validate_computation_in_interpolation!(@IS [NUMBER] $($expect_number)*); + validate_computation_in_interpolation!(@AFTER_IDENT $($rest)*); + }; + + // or else produce a nicer compiler error: "no rules expected the token `$invalid`" + (@AFTER_IDENT $invalid:tt $($rest:tt)*) => { + // no rule accepts a single token - voila! + validate_computation_in_interpolation!($invalid); + }; + + // ----------------------------------------- + // `@IS [NUMBER IDENT] something` will check whether `something` is either a number or an + // identifier + // + // Important: `@IS [NUMBER IDENT]` will work, but `@IS [IDENT NUMBER]` will fail, see + // - https://github.com/rust-lang/rust/issues/27832 + // + // The following will parse + // - @IS [NUMBER] 0 + // - @IS [IDENT] foo + // - @IS [NUMBER IDENT] 0 + // + // The following will not parse + // - @IS [NUMBER] foo + // - @IS [IDENT] 0 + // ----------------------------------------- + // if ident + (@IS [IDENT $($or:tt)*] $ident:ident) => {}; + // .. else continue + (@IS [IDENT $($or:tt)*] $($rest:tt)*) => { + validate_computation_in_interpolation!(@IS [$($or)*] $($rest)*); + }; + // if number in 0..32 + (@IS [NUMBER $($or:tt)*] 0) => {}; + (@IS [NUMBER $($or:tt)*] 1) => {}; + (@IS [NUMBER $($or:tt)*] 2) => {}; + (@IS [NUMBER $($or:tt)*] 3) => {}; + (@IS [NUMBER $($or:tt)*] 4) => {}; + (@IS [NUMBER $($or:tt)*] 5) => {}; + (@IS [NUMBER $($or:tt)*] 6) => {}; + (@IS [NUMBER $($or:tt)*] 7) => {}; + (@IS [NUMBER $($or:tt)*] 8) => {}; + (@IS [NUMBER $($or:tt)*] 9) => {}; + (@IS [NUMBER $($or:tt)*] 10) => {}; + (@IS [NUMBER $($or:tt)*] 11) => {}; + (@IS [NUMBER $($or:tt)*] 12) => {}; + (@IS [NUMBER $($or:tt)*] 13) => {}; + (@IS [NUMBER $($or:tt)*] 14) => {}; + (@IS [NUMBER $($or:tt)*] 15) => {}; + (@IS [NUMBER $($or:tt)*] 16) => {}; + (@IS [NUMBER $($or:tt)*] 17) => {}; + (@IS [NUMBER $($or:tt)*] 18) => {}; + (@IS [NUMBER $($or:tt)*] 19) => {}; + (@IS [NUMBER $($or:tt)*] 20) => {}; + (@IS [NUMBER $($or:tt)*] 21) => {}; + (@IS [NUMBER $($or:tt)*] 22) => {}; + (@IS [NUMBER $($or:tt)*] 23) => {}; + (@IS [NUMBER $($or:tt)*] 24) => {}; + (@IS [NUMBER $($or:tt)*] 25) => {}; + (@IS [NUMBER $($or:tt)*] 26) => {}; + (@IS [NUMBER $($or:tt)*] 27) => {}; + (@IS [NUMBER $($or:tt)*] 28) => {}; + (@IS [NUMBER $($or:tt)*] 29) => {}; + (@IS [NUMBER $($or:tt)*] 30) => {}; + (@IS [NUMBER $($or:tt)*] 31) => {}; + // .. else continue + (@IS [NUMBER $($or:tt)*] $($rest:tt)*) => { + validate_computation_in_interpolation!(@IS [$($or)*] $($rest)*); + }; + // if we reached `@IS []`, then nothing will match. + // + // Let's apply a trick to produce a nicer compiler error + (@IS [] $invalid:tt $($rest:tt)*) => { + // no rule accepts a single token - voila! + validate_computation_in_interpolation!($invalid); + }; +} + #[macro_export] #[doc(hidden)] macro_rules! quote_each_token { @@ -219,6 +352,14 @@ macro_rules! quote_each_token { quote_each_token!($tokens $($rest)*); }; + // wrap computations in a + ($tokens:ident # { $($inner:tt)* } $($rest:tt)*) => { + validate_computation_in_interpolation!(@ENTRY $($inner)*); + let computation = &$($inner)*; + $crate::ToTokens::to_tokens(computation, &mut $tokens); + quote_each_token!($tokens $($rest)*); + }; + ($tokens:ident # $first:ident $($rest:tt)*) => { $crate::ToTokens::to_tokens(&$first, &mut $tokens); quote_each_token!($tokens $($rest)*); diff --git a/tests/compile-fail/deny-closures.rs b/tests/compile-fail/deny-closures.rs new file mode 100644 index 0000000..214298f --- /dev/null +++ b/tests/compile-fail/deny-closures.rs @@ -0,0 +1,9 @@ +#[macro_use] +extern crate quote; + +fn main() { + quote!( + #{ foo.map(|x| &x) } + //~^ ERROR no rules expected the token `(` + ); +} diff --git a/tests/compile-fail/deny-macros.rs b/tests/compile-fail/deny-macros.rs new file mode 100644 index 0000000..fc1c49f --- /dev/null +++ b/tests/compile-fail/deny-macros.rs @@ -0,0 +1,9 @@ +#[macro_use] +extern crate quote; + +fn main() { + quote!( + #{ format!("Hello {}!", "world") } + //~^ ERROR no rules expected the token `!` + ); +} diff --git a/tests/compile-fail/deny-numbers.rs b/tests/compile-fail/deny-numbers.rs new file mode 100644 index 0000000..7ea4f4a --- /dev/null +++ b/tests/compile-fail/deny-numbers.rs @@ -0,0 +1,9 @@ +#[macro_use] +extern crate quote; + +fn main() { + quote!( + #{ 2*3*6 } + //~^ ERROR expected ident, found 2 + ); +} diff --git a/tests/compile-fail/deny-plus.rs b/tests/compile-fail/deny-plus.rs new file mode 100644 index 0000000..02fa199 --- /dev/null +++ b/tests/compile-fail/deny-plus.rs @@ -0,0 +1,9 @@ +#[macro_use] +extern crate quote; + +fn main() { + quote!( + #{ a+b } + //~^ ERROR no rules expected the token `+` + ); +} diff --git a/tests/compiletests.rs b/tests/compiletests.rs new file mode 100644 index 0000000..54c5e64 --- /dev/null +++ b/tests/compiletests.rs @@ -0,0 +1,43 @@ +#![cfg(feature = "dev_nightly")] +extern crate compiletest_rs as compiletest; + +// note: +// - `env::var("PROFILE")` is only available vor build scripts +// http://doc.crates.io/environment-variables.html +const PROFILE: &'static str = "debug"; + +use std::env; +use std::path::PathBuf; + +fn run_mode(mode: &'static str) { + let base_dir = env!("CARGO_MANIFEST_DIR"); + let test_dir = PathBuf::from(format!("{}/tests/{}", base_dir, mode)); + + if test_dir.is_dir() { + let mut config = compiletest::default_config(); + let cfg_mode = mode.parse().ok().expect("Invalid mode"); + + config.mode = cfg_mode; + config.src_base = test_dir; + + // note: + // - cargo respects the environment variable `env::var("CARGO_TARGET_DIR")`, + // however if this is not set and a virtual manifest is used, we will *not* + // know the path :-( + // In that case try to set `CARGO_TARGET_DIR` manually. + let build_dir = env::var("CARGO_TARGET_DIR").unwrap_or(base_dir.to_string()); + let artefacts_dir = format!("{}/target/{}", build_dir, PROFILE); + + config.target_rustcflags = + Some(format!("-L {} -L {}/deps", artefacts_dir, artefacts_dir)); + + compiletest::run_tests(&config); + } +} + +#[test] +fn compile_test() { + run_mode("run-pass"); + run_mode("run-fail"); + run_mode("compile-fail"); +} diff --git a/tests/computation.rs b/tests/computation.rs new file mode 100644 index 0000000..0259a9f --- /dev/null +++ b/tests/computation.rs @@ -0,0 +1,134 @@ +#[macro_use] +extern crate quote; + +pub use quote::Tokens; + +pub struct SimpleStruct1D { + x: T, +} + +pub struct SimpleStruct2D { + x: T, + y: T, +} + +mod simple { + #[allow(unused_imports)] + use super::*; + + #[test] + fn array_access() { + let a = [quote!(zero), quote!(one)]; + let b = [&a, &a]; + let tokens = quote!(#{a[0]} - #{a[1]} - #{b[1][0]} - #{b[0][1]}); + + assert_eq!(tokens, quote!( + zero - one - zero - one + )); + } + + #[test] + fn tuple_access() { + let a = (quote!(zero), quote!(one)); + let b = (&a, &a); + let tokens = quote!(#{a.0} - #{a.1} - #{(b.1).0} - #{(b.0).1}); + + assert_eq!(tokens, quote!( + zero - one - zero - one + )); + } + + #[test] + fn struct_access() { + let a = SimpleStruct2D { + x: quote!(zero), + y: quote!(one), + }; + let b = SimpleStruct2D { + x: &a, + y: &a, + }; + let tokens = quote!(#{a.x} - #{a.y} - #{b.y.x} - #{b.x.y}); + + assert_eq!(tokens, quote!( + zero - one - zero - one + )); + } + + #[test] + fn function_call() { + let a = || { quote!(Lorem) }; + let b = (&a,); + let c = [&a]; + let tokens = quote!(#{a()} - #{(b.0)()} - #{c[0]()}); + + assert_eq!(tokens, quote!( + Lorem - Lorem - Lorem + )); + } +} + +mod mixed { + #[allow(unused_imports)] + use super::*; + + #[test] + fn leading_array_access() { + let tuple = [(quote!(tuple),)]; + let strukt = [SimpleStruct1D { x: quote!(struct) }]; + let func = [|| { quote!(func) }]; + let tokens = quote!(#{tuple[0].0} - #{strukt[0].x} - #{func[0]()}); + + assert_eq!(tokens, quote!( + tuple - struct - func + )); + } + + #[test] + fn leading_tuple_access() { + let strukt = (SimpleStruct1D { x: quote!(struct) },); + let func = (|| { quote!(func) },); + let array = ([quote!(array)],); + let tokens = quote!(#{(strukt.0).x} - #{(func.0)()} - #{(array.0)[0]}); + + assert_eq!(tokens, quote!( + struct - func - array + )); + } + + #[test] + fn leading_struct_access() { + struct MixedStruct { + array: Vec, + tuple: (Tokens,), + } + + impl MixedStruct { + fn func(&self) -> Tokens { + quote!(func) + } + } + + let a = MixedStruct { + array: vec![quote!(array)], + tuple: (quote!(tuple),), + }; + let tokens = quote!(#{a.func()} - #{a.array[0]} - #{a.tuple.0}); + + assert_eq!(tokens, quote!( + func - array - tuple + )); + } + + #[test] + fn leading_function_call() { + let array = || { [quote!(array)] }; + let tuple = || { (quote!(tuple),) }; + let strukt = || { SimpleStruct1D { x: quote!(struct) } }; + let tokens = quote!(#{array()[0]} - #{tuple().0} - #{strukt().x}); + + assert_eq!(tokens, quote!( + array - tuple - struct + )); + } +}