proc_macro_warning/
lib.rs

1/*
2 * SPDX-FileCopyrightText: Oliver Tale-Yazdi <oliver@tasty.limo>
3 * SPDX-License-Identifier: (GPL-3.0 or Apache-2.0)
4 */
5
6#![doc = include_str!("../README.md")]
7#![deny(unsafe_code)]
8#![deny(missing_docs)]
9
10use proc_macro2::Span;
11use quote::{quote_spanned, ToTokens};
12use syn::Ident;
13
14mod test;
15
16/// Creates a compile-time warning for proc macro use. See [DeprecatedWarningBuilder] for usage.
17#[derive(Clone)]
18#[cfg_attr(feature = "derive_debug", derive(Debug))]
19pub enum Warning {
20	/// A *deprecation* warning that notifies users of outdated types and functions.
21	Deprecated {
22		/// The name of the warning.
23		name: String,
24		/// The index of the warning. Name++Index must be unique.
25		index: Option<usize>,
26		/// The message of the warning.
27		message: String,
28		/// The help links to be displayed next to the message.
29		links: Vec<String>,
30		/// The span of the warning.
31		span: Span,
32	},
33}
34
35/// A compile-time warning that was already subject to formatting.
36///
37/// Any content will be pasted as-is.
38#[derive(Clone)]
39#[cfg_attr(feature = "derive_debug", derive(Debug))]
40pub enum FormattedWarning {
41	/// A *deprecation* warning.
42	Deprecated {
43		/// Unique name of this warning.
44		///
45		/// Must be unique in the case that multiple of these warnings are emitted, for example by
46		/// appending a counter.
47		name: Ident,
48		/// The exact note to be used for `note = ""`.
49		note: String,
50		/// The span of the warning.
51		///
52		/// Should be set to the original location of where the warning should be emitted.
53		span: Option<Span>,
54	},
55}
56
57impl FormattedWarning {
58	/// Create a new deprecated warning that already was formatted by the caller.
59	#[must_use]
60	pub fn new_deprecated<S, T>(name: S, note: T, span: Span) -> Self
61	where
62		S: AsRef<str>,
63		T: Into<String>,
64	{
65		Self::Deprecated {
66			name: Ident::new(name.as_ref(), span),
67			note: note.into(),
68			span: Some(span),
69		}
70	}
71}
72
73/// Gradually build a *deprecation* `Warning`.
74///
75/// # Example
76///
77/// ```rust
78/// use proc_macro_warning::Warning;
79///
80/// let warning = Warning::new_deprecated("my_macro")
81///     .old("my_macro()")
82///     .new("my_macro::new()")
83///     .help_link("https:://example.com")
84///     // Normally you use the input span, but this is an example:
85///     .span(proc_macro2::Span::call_site())
86///     .build_or_panic();
87///
88/// let mut warnings = vec![warning];
89/// // When adding more, you will need to build each with `.index`.
90///
91/// // In a proc macro you can expand them in a private module:
92/// quote::quote! {
93///     mod warnings {
94///         #(
95///             #warnings
96///         )*
97///     }
98/// };
99/// ```
100#[derive(Default, Clone)]
101#[cfg_attr(feature = "derive_debug", derive(Debug))]
102pub struct DeprecatedWarningBuilder {
103	title: String,
104	index: Option<usize>,
105	old: Option<String>,
106	new: Option<String>,
107	links: Vec<String>,
108	span: Option<Span>,
109}
110
111impl DeprecatedWarningBuilder {
112	/// Create a new *deprecated* warning builder with the given title.
113	///
114	/// The title must be unique for each warning.
115	#[must_use]
116	pub fn from_title<S: Into<String>>(title: S) -> Self {
117		Self { title: title.into(), ..Default::default() }
118	}
119
120	/// Set an optional index in case that a warning appears multiple times.
121	///
122	/// Must be set if a warning appears multiple times.
123	#[must_use]
124	pub fn index<S: Into<usize>>(self, index: S) -> Self {
125		Self { index: Some(index.into()), ..self }
126	}
127
128	/// The old *deprecated* way of doing something.
129	///
130	/// Should complete the sentence "It is deprecated to ...".
131	#[must_use]
132	pub fn old<S: Into<String>>(self, old: S) -> Self {
133		Self { old: Some(old.into()), ..self }
134	}
135
136	/// The *new* way of doing something.
137	///
138	/// Should complete the sentence "Please instead ...".
139	#[must_use]
140	pub fn new<S: Into<String>>(self, new: S) -> Self {
141		Self { new: Some(new.into()), ..self }
142	}
143
144	/// A help link for the user to explain the transition and justification.
145	#[must_use]
146	pub fn help_link<S: Into<String>>(self, link: S) -> Self {
147		Self { links: vec![link.into()], ..self }
148	}
149
150	/// Multiple help links for the user to explain the transition and justification.
151	#[must_use]
152	pub fn help_links(self, links: &[&str]) -> Self {
153		Self { links: links.iter().map(|s| (*s).into()).collect(), ..self }
154	}
155
156	/// Set the span of the warning.
157	#[must_use]
158	pub fn span(self, span: Span) -> Self {
159		Self { span: Some(span), ..self }
160	}
161
162	/// Fallibly build a warning.
163	#[deprecated(note = "Use `try_build` instead; Will be removed after Q1 2024")]
164	pub fn maybe_build(self) -> Result<Warning, String> {
165		self.try_build()
166	}
167
168	/// Try to build the warning.
169	pub fn try_build(self) -> Result<Warning, String> {
170		let span = self.span.unwrap_or_else(Span::call_site);
171		let title = self.title;
172		let old = self.old.ok_or("Missing old")?;
173		let new = self.new.ok_or("Missing new")?;
174		let message = format!("It is deprecated to {}.\nPlease instead {}.", old, new);
175
176		Ok(Warning::Deprecated { name: title, index: self.index, message, links: self.links, span })
177	}
178
179	/// Unwraps [`Self::maybe_build`] for convenience.
180	#[must_use]
181	#[deprecated(note = "Use `build_or_panic` instead; Will be removed after Q1 2024")]
182	pub fn build(self) -> Warning {
183		self.build_or_panic()
184	}
185
186	/// Build the warning or panic if it fails.
187	#[must_use]
188	pub fn build_or_panic(self) -> Warning {
189		self.try_build().expect("maybe_build failed")
190	}
191}
192
193impl Warning {
194	/// Create a new *deprecated* warning.
195	#[must_use]
196	pub fn new_deprecated<S: Into<String>>(title: S) -> DeprecatedWarningBuilder {
197		DeprecatedWarningBuilder::from_title(title)
198	}
199
200	/// Sanitize the warning message.
201	fn final_deprecated_message(&self) -> String {
202		let (message, links) = match self {
203			Self::Deprecated { message, links, .. } => (message, links),
204		};
205
206		let lines = message.trim().lines().map(|line| line.trim_start());
207		// Prepend two tabs to each line
208		let message = lines.map(|line| format!("\t\t{}", line)).collect::<Vec<_>>().join("\n");
209
210		if !links.is_empty() {
211			let link =
212				links.iter().map(|l| format!("<{}>", l)).collect::<Vec<_>>().join("\n\t\t\t");
213			format!("\n{}\n\n\t\tFor more info see:\n\t\t\t{}", message, link)
214		} else {
215			format!("\n{}", message)
216		}
217	}
218
219	/// Sanitize the warning name.
220	fn final_deprecated_name(&self) -> Ident {
221		let (index, name, span) = match self {
222			Self::Deprecated { index, name, span, .. } => (*index, name, *span),
223		};
224
225		let name = match index {
226			Some(i) => format!("{}_{}", name, i),
227			None => name.clone(),
228		};
229
230		Ident::new(&name, span)
231	}
232}
233
234impl From<Warning> for FormattedWarning {
235	fn from(val: Warning) -> Self {
236		match val {
237			Warning::Deprecated { span, .. } => Self::Deprecated {
238				name: val.final_deprecated_name(),
239				note: val.final_deprecated_message(),
240				span: Some(span),
241			},
242		}
243	}
244}
245
246impl ToTokens for Warning {
247	fn to_tokens(&self, stream: &mut proc_macro2::TokenStream) {
248		let formatted: FormattedWarning = self.clone().into();
249		formatted.to_tokens(stream);
250	}
251}
252
253impl ToTokens for FormattedWarning {
254	fn to_tokens(&self, stream: &mut proc_macro2::TokenStream) {
255		let (name, note, span) = match self {
256			Self::Deprecated { name, note, span } => (name, note, span),
257		};
258		let span = span.unwrap_or_else(Span::call_site);
259
260		let q = quote_spanned!(span =>
261			/// This function should not be called and only exists to emit a compiler warning.
262			///
263			/// It is a No-OP in any case.
264			#[allow(dead_code)]
265			#[allow(non_camel_case_types)]
266			#[allow(non_snake_case)]
267			fn #name() {
268				#[deprecated(note = #note)]
269				#[allow(non_upper_case_globals)]
270				const _w: () = ();
271				let _ = _w;
272			}
273		);
274		q.to_tokens(stream);
275	}
276}