diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 312bf69..8b7bd22 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -26,11 +26,14 @@ jobs: - name: Format run: cargo fmt --all -- --check + - name: Check no-std + run: cargo check --all-targets --no-default-features # still broken --locked + - name: Build - run: cargo build --all-targets --all-features # --locked TODO + run: cargo build --all-targets --all-features --locked - name: Test - run: cargo test --locked --all-targets --all-features + run: TRYBUILD=overwrite cargo test --locked --all-targets --all-features - name: Clippy run: cargo clippy --locked --all-targets --all-features -- -D warnings diff --git a/.gitignore b/.gitignore index cd23ebe..48be6a6 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,7 @@ /target/ +**/target + Cargo.lock **/*.rs.bk + +.vscode/ diff --git a/Cargo.toml b/Cargo.toml index a3abc5d..0f69d47 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,18 +1,7 @@ -[package] -name = "proc-macro-warning" -version = "0.3.1" -edition = "2021" -license = "GPL-3.0 OR Apache-2.0" -authors = ["Oliver Tale-Yazdi "] -description = "Emit warnings from inside proc macros." -repository = "https://github.com/ggwpez/proc-macro-warning" +[workspace] -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html - -[dependencies] -proc-macro2 = { version = "1.0.56", default-features = false } -quote = { version = "1.0.26", default-features = false } -syn = { version = "2.0.15", default-features = false } - -[features] -default = [] +members = [ + "proc-macro-warning", + "ui-tests/derive", + "ui-tests/ui", +] diff --git a/README.md b/README.md index 3b56824..c0161c3 100644 --- a/README.md +++ b/README.md @@ -24,6 +24,7 @@ Building a warning is easy with the builder pattern. ```rust use proc_macro_warning::Warning; + let warning = Warning::new_deprecated("my_macro") .old("my_macro()") .new("my_macro::new()") @@ -35,11 +36,45 @@ let warning = Warning::new_deprecated("my_macro") let tokens = quote::quote!(#warning); ``` +This works in derive-macros, but you **must** set a span; otherwise it will not show up in the compile output. + +The difference to a `#[deprecated]` attribute is that it emits the warning either way. For example when creating a custom `Deprecated` derive macro, it will warn without the struct being constructed. + +```rust +#[derive(derive::Deprecated)] +struct Test {} + +fn main() { + // Warning triggers although we never used `Test`. + // Otherwise use a normal `#[deprecated]`. +} +``` + +## Un-opinionated Formatting + +The normal aforementioned way of creating a warning will impose specific unified grammar and formatting rules. +You can opt out of this and use your own instead by using `FormattedWarning::new_deprecated`: + +```rust +use proc_macro_warning::FormattedWarning; + +let warning = FormattedWarning::new_deprecated( + "my_macro", + "looooooooooooooooooooooooooooooong line that will not be brokeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeen ;)", + proc_macro2::Span::call_site(), + ); + +// Use the warning in a proc macro +let tokens = quote::quote!(#warning); +``` + +The output of a [similar example](ui-tests/derive/src/lib.rs) is in [derive_raw.stderr](ui-tests/ui/src/warn/derive_raw.stderr). + ## Used In -Substrate (since [#13798](https://github.com/paritytech/substrate/pull/13798)) uses this to emit warnings for its FRAME eDSL on deprecated behaviour. +Substrate uses it to emit warnings for its eDSL (FRAME) on deprecated behaviour. The integration was done in [#13798](https://github.com/paritytech/substrate/pull/13798) and shows how to use these warnings in macro expansion. -For example not putting a `call_index` on your functions produces: +The warnings are uniformly formatted and have consistent grammar: ```pre warning: use of deprecated constant `pallet::warnings::ImplicitCallIndex_0::_w`: It is deprecated to use implicit call indices. @@ -55,7 +90,7 @@ warning: use of deprecated constant `pallet::warnings::ImplicitCallIndex_0::_w`: | ``` -Or using a hard-coded weight: +A different one: ```pre warning: use of deprecated constant `pallet::warnings::ConstantWeight_0::_w`: It is deprecated to use hard-coded constant as call weight. @@ -69,13 +104,12 @@ warning: use of deprecated constant `pallet::warnings::ConstantWeight_0::_w`: | ``` - ## License Licensed under either of at your own choice: -* GNU GENERAL PUBLIC LICENSE, Version 3 ([LICENSE-GPL3](./LICENSE-GPL3) or https://www.gnu.org/licenses/gpl-3.0.txt) -* Apache License, Version 2.0 ([LICENSE-APACHE2](/LICENSE-APACHE2) or https://www.apache.org/licenses/LICENSE-2.0.txt). +* GNU GENERAL PUBLIC LICENSE, Version 3 ([LICENSE-GPL3](./LICENSE-GPL3) or [gnu.org](https://www.gnu.org/licenses/gpl-3.0.txt>)) +* Apache License, Version 2.0 ([LICENSE-APACHE2](/LICENSE-APACHE2) or [apache.org](https://www.apache.org/licenses/LICENSE-2.0.txt>)). ### Contribution diff --git a/proc-macro-warning/Cargo.toml b/proc-macro-warning/Cargo.toml new file mode 100644 index 0000000..dc6d547 --- /dev/null +++ b/proc-macro-warning/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "proc-macro-warning" +version = "0.4.0" +edition = "2021" +license = "GPL-3.0 OR Apache-2.0" +authors = ["Oliver Tale-Yazdi "] +description = "Emit warnings from inside proc macros." +repository = "https://github.com/ggwpez/proc-macro-warning" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +proc-macro2 = { version = "1.0.56", default-features = false } +quote = { version = "1.0.27", default-features = false } +syn = { version = "2.0.16", default-features = false } + +[dev-dependencies] +derive = { path = "../ui-tests/derive" } + +[features] +default = [] diff --git a/src/lib.rs b/proc-macro-warning/src/lib.rs similarity index 58% rename from src/lib.rs rename to proc-macro-warning/src/lib.rs index 92c7750..dc6010a 100644 --- a/src/lib.rs +++ b/proc-macro-warning/src/lib.rs @@ -3,7 +3,7 @@ * SPDX-License-Identifier: (GPL-3.0 or Apache-2.0) */ -//! Emit warnings from inside proc macros. +#![doc = include_str!("../../README.md")] use core::ops::Deref; use proc_macro2::Span; @@ -12,32 +12,74 @@ use quote::{quote_spanned, ToTokens}; mod test; /// Creates a compile-time warning for proc macro use. See [DeprecatedWarningBuilder] for usage. -pub struct Warning { - pub name: String, - pub index: Option, - pub message: String, - pub links: Vec, - pub span: Span, +#[derive(Clone)] +pub enum Warning { + /// A *deprecation* warning that notifies users of outdated types and functions. + Deprecated { + name: String, + index: Option, + message: String, + links: Vec, + span: Span, + }, } -/// Gradually build a "deprecated" `Warning`. +/// A compile-time warning that was already subject to formatting. +/// +/// Any content will be pasted as-is. +#[derive(Clone)] +pub enum FormattedWarning { + /// A *deprecation* warning. + Deprecated { + /// Unique name of this warning. + /// + /// Must be unique in the case that multiple of these warnings are emitted, for example by + /// appending a counter. + name: syn::Ident, + /// The exact note to be used for `note = ""`. + note: String, + /// The span of the warning. + /// + /// Should be set to the original location of where the warning should be emitted. + span: Option, + }, +} + +impl FormattedWarning { + /// Create a new deprecated warning that already was formatted by the caller. + #[must_use] + pub fn new_deprecated<'a, S, T>(name: S, note: T, span: Span) -> Self + where + S: Into<&'a str>, + T: Into, + { + Self::Deprecated { + name: syn::Ident::new(name.into(), span), + note: note.into(), + span: Some(span), + } + } +} + +/// Gradually build a *deprecation* `Warning`. /// /// # Example -/// ``` +/// +/// ```rust /// use proc_macro_warning::Warning; /// /// let warning = Warning::new_deprecated("my_macro") /// .old("my_macro()") /// .new("my_macro::new()") /// .help_link("https:://example.com") +/// // Normally you use the input span, but this is an example: /// .span(proc_macro2::Span::call_site()) /// .build(); /// -/// // Use the warning in a proc macro -/// let tokens = quote::quote!(#warning); +/// let mut warnings = vec![warning]; +/// // When adding more, you will need to build each with `.index`. /// -/// let warnings = vec![warning]; -/// // In a proc macro you would expand them inside a module: +/// // In a proc macro you can expand them in a private module: /// quote::quote! { /// mod warnings { /// #( @@ -115,7 +157,7 @@ impl DeprecatedWarningBuilder { let new = self.new.ok_or("Missing new")?; let message = format!("It is deprecated to {}.\nPlease instead {}.", old, new); - Ok(Warning { name: title, index: self.index, message, links: self.links, span }) + Ok(Warning::Deprecated { name: title, index: self.index, message, links: self.links, span }) } /// Unwraps [`Self::maybe_build`] for convenience. @@ -126,17 +168,6 @@ impl DeprecatedWarningBuilder { } impl Warning { - /// Create a new *raw* warnings. - pub fn new_raw( - name: String, - index: Option, - message: String, - help_links: Vec, - span: Span, - ) -> Warning { - Warning { name, index, message, links: help_links, span } - } - /// Create a new *deprecated* warning. #[must_use] pub fn new_deprecated(title: &str) -> DeprecatedWarningBuilder { @@ -144,18 +175,18 @@ impl Warning { } /// Sanitize the warning message. - fn final_message(&self) -> String { - let lines = self.message.trim().lines().map(|line| line.trim_start()); + fn final_deprecated_message(&self) -> String { + let (message, links) = match self { + Warning::Deprecated { message, links, .. } => (message, links), + }; + + let lines = message.trim().lines().map(|line| line.trim_start()); // Prepend two tabs to each line let message = lines.map(|line| format!("\t\t{}", line)).collect::>().join("\n"); - if !self.links.is_empty() { - let link = self - .links - .iter() - .map(|l| format!("<{}>", l)) - .collect::>() - .join("\n\t\t\t"); + if !links.is_empty() { + let link = + links.iter().map(|l| format!("<{}>", l)).collect::>().join("\n\t\t\t"); format!("\n{}\n\n\t\tFor more info see:\n\t\t\t{}", message, link) } else { format!("\n{}", message) @@ -163,29 +194,54 @@ impl Warning { } /// Sanitize the warning name. - fn final_name(&self) -> syn::Ident { - let name = match self.index { - Some(i) => format!("{}_{}", self.name, i), - None => self.name.clone(), + fn final_deprecated_name(&self) -> syn::Ident { + let (index, name, span) = match self { + Warning::Deprecated { index, name, span, .. } => (*index, name, *span), + }; + + let name = match index { + Some(i) => format!("{}_{}", name, i), + None => name.clone(), }; - syn::Ident::new(&name, self.span) + syn::Ident::new(&name, span) + } +} + +impl From for FormattedWarning { + fn from(val: Warning) -> Self { + match val { + Warning::Deprecated { span, .. } => FormattedWarning::Deprecated { + name: val.final_deprecated_name(), + note: val.final_deprecated_message(), + span: Some(span), + }, + } } } impl ToTokens for Warning { fn to_tokens(&self, stream: &mut proc_macro2::TokenStream) { - let name = self.final_name(); - let message = self.final_message(); + let formatted: FormattedWarning = self.clone().into(); + formatted.to_tokens(stream); + } +} + +impl ToTokens for FormattedWarning { + fn to_tokens(&self, stream: &mut proc_macro2::TokenStream) { + let (name, note, span) = match self { + FormattedWarning::Deprecated { name, note, span } => (name, note, span), + }; + let span = span.unwrap_or_else(Span::call_site); - let q = quote_spanned!(self.span => + let q = quote_spanned!(span => /// This function should not be called and only exists to emit a compiler warning. /// - /// It is a No-OP if you want try it anyway ;) + /// It is a No-OP in any case. #[allow(dead_code)] #[allow(non_camel_case_types)] #[allow(non_snake_case)] fn #name() { - #[deprecated(note = #message)] + #[deprecated(note = #note)] #[allow(non_upper_case_globals)] const _w: () = (); let _ = _w; diff --git a/src/test.rs b/proc-macro-warning/src/test.rs similarity index 95% rename from src/test.rs rename to proc-macro-warning/src/test.rs index dc7f04d..e7624a7 100644 --- a/src/test.rs +++ b/proc-macro-warning/src/test.rs @@ -22,7 +22,7 @@ fn example_works() { let want_tokens = quote!( /// This function should not be called and only exists to emit a compiler warning. /// - /// It is a No-OP if you want try it anyway ;) + /// It is a No-OP in any case. #[allow(dead_code)] #[allow(non_camel_case_types)] #[allow(non_snake_case)] diff --git a/ui-tests/Cargo.toml b/ui-tests/Cargo.toml new file mode 100644 index 0000000..061fc0b --- /dev/null +++ b/ui-tests/Cargo.toml @@ -0,0 +1,5 @@ +[workspace] +members = [ + "derive", + "ui" +] diff --git a/ui-tests/derive/Cargo.toml b/ui-tests/derive/Cargo.toml new file mode 100644 index 0000000..33405e8 --- /dev/null +++ b/ui-tests/derive/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "derive" +version = "0.1.0" +edition = "2021" +publish = false + +[lib] +proc-macro = true + +[dependencies] +proc-macro-warning = { path = "../../proc-macro-warning" } +quote = "1.0.27" +syn = "2.0.16" diff --git a/ui-tests/derive/src/lib.rs b/ui-tests/derive/src/lib.rs new file mode 100644 index 0000000..525c619 --- /dev/null +++ b/ui-tests/derive/src/lib.rs @@ -0,0 +1,43 @@ +//! TESTING ONLY - DO NOT USE. + +use proc_macro::TokenStream; +use quote::ToTokens; +use syn::spanned::Spanned; + +#[proc_macro_derive(Deprecated)] +pub fn deprecated(input: TokenStream) -> TokenStream { + impl_dep(input, true) +} + +#[proc_macro_derive(DeprecatedNoSpan)] +pub fn deprecated2(input: TokenStream) -> TokenStream { + impl_dep(input, false) +} + +fn impl_dep(input: TokenStream, span: bool) -> TokenStream { + let input = syn::parse_macro_input!(input as syn::DeriveInput); + + let warning = proc_macro_warning::Warning::new_deprecated("test").old("foo").new("bar"); + let warning = if span { warning.span(input.span()) } else { warning }.build(); + + warning.into_token_stream().into() +} + +#[proc_macro_derive(DeprecatedRaw)] +pub fn deprecated_raw(input: TokenStream) -> TokenStream { + impl_dep_raw(input) +} + +fn impl_dep_raw(input: TokenStream) -> TokenStream { + let input = syn::parse_macro_input!(input as syn::DeriveInput); + + let warning = proc_macro_warning::FormattedWarning::new_deprecated( + "VeryOldStuff", + "\nMy message do noooooooooooooooooooooooooooooot formaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaat +or chaaaaaaaaaaaaange this, also no line breaks please ;) +other veryyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy looooooooooooooooooooooong lineeeeeeeeeeeeeeee", + input.span(), + ); + + warning.into_token_stream().into() +} diff --git a/ui-tests/ui/Cargo.toml b/ui-tests/ui/Cargo.toml new file mode 100644 index 0000000..7c1c9b6 --- /dev/null +++ b/ui-tests/ui/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "ui" +version = "0.1.0" +edition = "2021" +publish = false + +[dev-dependencies] +trybuild = "1.0.80" +derive = { path = "../derive" } diff --git a/ui-tests/ui/src/lib.rs b/ui-tests/ui/src/lib.rs new file mode 100644 index 0000000..649a475 --- /dev/null +++ b/ui-tests/ui/src/lib.rs @@ -0,0 +1,19 @@ +//! TESTING ONLY - DO NOT USE. + +#[test] +#[cfg(test)] +fn ui_warm() { + std::env::set_var("RUSTFLAGS", "--deny warnings"); + let t = trybuild::TestCases::new(); + + t.compile_fail("src/warn/*.rs"); +} + +#[test] +#[cfg(test)] +fn ui_no_warn() { + std::env::set_var("RUSTFLAGS", "--deny warnings"); + let t = trybuild::TestCases::new(); + + t.pass("src/no-warn/*.rs"); +} diff --git a/ui-tests/ui/src/no-warn/derive_no_span.rs b/ui-tests/ui/src/no-warn/derive_no_span.rs new file mode 100644 index 0000000..05a0bb7 --- /dev/null +++ b/ui-tests/ui/src/no-warn/derive_no_span.rs @@ -0,0 +1,10 @@ +//! Without a span no warning will be printed. + +#[derive(derive::DeprecatedNoSpan)] +struct Test { + +} + +fn main() { + let _ = Test { }; +} diff --git a/ui-tests/ui/src/warn/derive_basic.rs b/ui-tests/ui/src/warn/derive_basic.rs new file mode 100644 index 0000000..b14fcd1 --- /dev/null +++ b/ui-tests/ui/src/warn/derive_basic.rs @@ -0,0 +1,5 @@ +#[derive(derive::Deprecated)] +struct Test; + +fn main() { +} diff --git a/ui-tests/ui/src/warn/derive_basic.stderr b/ui-tests/ui/src/warn/derive_basic.stderr new file mode 100644 index 0000000..abd9ebb --- /dev/null +++ b/ui-tests/ui/src/warn/derive_basic.stderr @@ -0,0 +1,9 @@ +error: use of deprecated constant `test::_w`: + It is deprecated to foo. + Please instead bar. + --> src/warn/derive_basic.rs:2:1 + | +2 | struct Test; + | ^^^^^^^^^^^^ + | + = note: `-D deprecated` implied by `-D warnings` diff --git a/ui-tests/ui/src/warn/derive_raw.rs b/ui-tests/ui/src/warn/derive_raw.rs new file mode 100644 index 0000000..11f90d0 --- /dev/null +++ b/ui-tests/ui/src/warn/derive_raw.rs @@ -0,0 +1,5 @@ +#[derive(derive::DeprecatedRaw)] +struct Test; + +fn main() { +} diff --git a/ui-tests/ui/src/warn/derive_raw.stderr b/ui-tests/ui/src/warn/derive_raw.stderr new file mode 100644 index 0000000..a8b6551 --- /dev/null +++ b/ui-tests/ui/src/warn/derive_raw.stderr @@ -0,0 +1,10 @@ +error: use of deprecated constant `VeryOldStuff::_w`: + My message do noooooooooooooooooooooooooooooot formaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaat + or chaaaaaaaaaaaaange this, also no line breaks please ;) + other veryyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy looooooooooooooooooooooong lineeeeeeeeeeeeeeee + --> src/warn/derive_raw.rs:2:1 + | +2 | struct Test; + | ^^^^^^^^^^^^ + | + = note: `-D deprecated` implied by `-D warnings` diff --git a/ui-tests/ui/src/warn/derive_twice_errors.rs b/ui-tests/ui/src/warn/derive_twice_errors.rs new file mode 100644 index 0000000..aa46688 --- /dev/null +++ b/ui-tests/ui/src/warn/derive_twice_errors.rs @@ -0,0 +1,8 @@ +#[derive(derive::Deprecated)] +struct Test; + +#[derive(derive::Deprecated)] +struct Test2; + +fn main() { +} diff --git a/ui-tests/ui/src/warn/derive_twice_errors.stderr b/ui-tests/ui/src/warn/derive_twice_errors.stderr new file mode 100644 index 0000000..190b49c --- /dev/null +++ b/ui-tests/ui/src/warn/derive_twice_errors.stderr @@ -0,0 +1,28 @@ +error[E0428]: the name `test` is defined multiple times + --> src/warn/derive_twice_errors.rs:5:1 + | +2 | struct Test; + | ------------ previous definition of the value `test` here +... +5 | struct Test2; + | ^^^^^^^^^^^^^ `test` redefined here + | + = note: `test` must be defined only once in the value namespace of this module + +error: use of deprecated constant `test::_w`: + It is deprecated to foo. + Please instead bar. + --> src/warn/derive_twice_errors.rs:2:1 + | +2 | struct Test; + | ^^^^^^^^^^^^ + | + = note: `-D deprecated` implied by `-D warnings` + +error: use of deprecated constant `test::_w`: + It is deprecated to foo. + Please instead bar. + --> src/warn/derive_twice_errors.rs:5:1 + | +5 | struct Test2; + | ^^^^^^^^^^^^^