]>
Commit | Line | Data |
---|---|---|
2b03887a FG |
1 | use crate::diagnostics::error::{ |
2 | span_err, throw_invalid_attr, throw_invalid_nested_attr, throw_span_err, DiagnosticDeriveError, | |
3 | }; | |
04454e1e | 4 | use proc_macro::Span; |
2b03887a | 5 | use proc_macro2::{Ident, TokenStream}; |
04454e1e | 6 | use quote::{format_ident, quote, ToTokens}; |
2b03887a | 7 | use std::cell::RefCell; |
064997fb | 8 | use std::collections::{BTreeSet, HashMap}; |
2b03887a | 9 | use std::fmt; |
04454e1e | 10 | use std::str::FromStr; |
2b03887a FG |
11 | use syn::{spanned::Spanned, Attribute, Field, Meta, Type, TypeTuple}; |
12 | use syn::{MetaList, MetaNameValue, NestedMeta, Path}; | |
13 | use synstructure::{BindingInfo, VariantInfo}; | |
14 | ||
487cf647 | 15 | use super::error::{invalid_attr, invalid_nested_attr}; |
2b03887a FG |
16 | |
17 | thread_local! { | |
18 | pub static CODE_IDENT_COUNT: RefCell<u32> = RefCell::new(0); | |
19 | } | |
20 | ||
21 | /// Returns an ident of the form `__code_N` where `N` is incremented once with every call. | |
22 | pub(crate) fn new_code_ident() -> syn::Ident { | |
23 | CODE_IDENT_COUNT.with(|count| { | |
24 | let ident = format_ident!("__code_{}", *count.borrow()); | |
25 | *count.borrow_mut() += 1; | |
26 | ident | |
27 | }) | |
28 | } | |
04454e1e FG |
29 | |
30 | /// Checks whether the type name of `ty` matches `name`. | |
31 | /// | |
32 | /// Given some struct at `a::b::c::Foo`, this will return true for `c::Foo`, `b::c::Foo`, or | |
33 | /// `a::b::c::Foo`. This reasonably allows qualified names to be used in the macro. | |
34 | pub(crate) fn type_matches_path(ty: &Type, name: &[&str]) -> bool { | |
35 | if let Type::Path(ty) = ty { | |
36 | ty.path | |
37 | .segments | |
38 | .iter() | |
39 | .map(|s| s.ident.to_string()) | |
40 | .rev() | |
41 | .zip(name.iter().rev()) | |
42 | .all(|(x, y)| &x.as_str() == y) | |
43 | } else { | |
44 | false | |
45 | } | |
46 | } | |
47 | ||
48 | /// Checks whether the type `ty` is `()`. | |
49 | pub(crate) fn type_is_unit(ty: &Type) -> bool { | |
50 | if let Type::Tuple(TypeTuple { elems, .. }) = ty { elems.is_empty() } else { false } | |
51 | } | |
52 | ||
53 | /// Reports a type error for field with `attr`. | |
54 | pub(crate) fn report_type_error( | |
55 | attr: &Attribute, | |
56 | ty_name: &str, | |
064997fb | 57 | ) -> Result<!, DiagnosticDeriveError> { |
04454e1e FG |
58 | let name = attr.path.segments.last().unwrap().ident.to_string(); |
59 | let meta = attr.parse_meta()?; | |
60 | ||
61 | throw_span_err!( | |
62 | attr.span().unwrap(), | |
63 | &format!( | |
64 | "the `#[{}{}]` attribute can only be applied to fields of type {}", | |
65 | name, | |
66 | match meta { | |
67 | Meta::Path(_) => "", | |
68 | Meta::NameValue(_) => " = ...", | |
69 | Meta::List(_) => "(...)", | |
70 | }, | |
71 | ty_name | |
72 | ) | |
73 | ); | |
74 | } | |
75 | ||
76 | /// Reports an error if the field's type does not match `path`. | |
77 | fn report_error_if_not_applied_to_ty( | |
78 | attr: &Attribute, | |
79 | info: &FieldInfo<'_>, | |
80 | path: &[&str], | |
81 | ty_name: &str, | |
064997fb | 82 | ) -> Result<(), DiagnosticDeriveError> { |
487cf647 | 83 | if !type_matches_path(info.ty, path) { |
04454e1e FG |
84 | report_type_error(attr, ty_name)?; |
85 | } | |
86 | ||
87 | Ok(()) | |
88 | } | |
89 | ||
90 | /// Reports an error if the field's type is not `Applicability`. | |
91 | pub(crate) fn report_error_if_not_applied_to_applicability( | |
92 | attr: &Attribute, | |
93 | info: &FieldInfo<'_>, | |
064997fb | 94 | ) -> Result<(), DiagnosticDeriveError> { |
04454e1e FG |
95 | report_error_if_not_applied_to_ty( |
96 | attr, | |
97 | info, | |
98 | &["rustc_errors", "Applicability"], | |
99 | "`Applicability`", | |
100 | ) | |
101 | } | |
102 | ||
103 | /// Reports an error if the field's type is not `Span`. | |
104 | pub(crate) fn report_error_if_not_applied_to_span( | |
105 | attr: &Attribute, | |
106 | info: &FieldInfo<'_>, | |
064997fb | 107 | ) -> Result<(), DiagnosticDeriveError> { |
487cf647 FG |
108 | if !type_matches_path(info.ty, &["rustc_span", "Span"]) |
109 | && !type_matches_path(info.ty, &["rustc_errors", "MultiSpan"]) | |
064997fb FG |
110 | { |
111 | report_type_error(attr, "`Span` or `MultiSpan`")?; | |
112 | } | |
113 | ||
114 | Ok(()) | |
04454e1e FG |
115 | } |
116 | ||
117 | /// Inner type of a field and type of wrapper. | |
118 | pub(crate) enum FieldInnerTy<'ty> { | |
119 | /// Field is wrapped in a `Option<$inner>`. | |
120 | Option(&'ty Type), | |
121 | /// Field is wrapped in a `Vec<$inner>`. | |
122 | Vec(&'ty Type), | |
123 | /// Field isn't wrapped in an outer type. | |
124 | None, | |
125 | } | |
126 | ||
127 | impl<'ty> FieldInnerTy<'ty> { | |
128 | /// Returns inner type for a field, if there is one. | |
129 | /// | |
130 | /// - If `ty` is an `Option`, returns `FieldInnerTy::Option { inner: (inner type) }`. | |
131 | /// - If `ty` is a `Vec`, returns `FieldInnerTy::Vec { inner: (inner type) }`. | |
132 | /// - Otherwise returns `None`. | |
133 | pub(crate) fn from_type(ty: &'ty Type) -> Self { | |
134 | let variant: &dyn Fn(&'ty Type) -> FieldInnerTy<'ty> = | |
135 | if type_matches_path(ty, &["std", "option", "Option"]) { | |
136 | &FieldInnerTy::Option | |
137 | } else if type_matches_path(ty, &["std", "vec", "Vec"]) { | |
138 | &FieldInnerTy::Vec | |
139 | } else { | |
140 | return FieldInnerTy::None; | |
141 | }; | |
142 | ||
143 | if let Type::Path(ty_path) = ty { | |
144 | let path = &ty_path.path; | |
145 | let ty = path.segments.iter().last().unwrap(); | |
146 | if let syn::PathArguments::AngleBracketed(bracketed) = &ty.arguments { | |
147 | if bracketed.args.len() == 1 { | |
148 | if let syn::GenericArgument::Type(ty) = &bracketed.args[0] { | |
149 | return variant(ty); | |
150 | } | |
151 | } | |
152 | } | |
153 | } | |
154 | ||
155 | unreachable!(); | |
156 | } | |
157 | ||
2b03887a FG |
158 | /// Returns `true` if `FieldInnerTy::with` will result in iteration for this inner type (i.e. |
159 | /// that cloning might be required for values moved in the loop body). | |
160 | pub(crate) fn will_iterate(&self) -> bool { | |
161 | match self { | |
162 | FieldInnerTy::Vec(..) => true, | |
163 | FieldInnerTy::Option(..) | FieldInnerTy::None => false, | |
164 | } | |
165 | } | |
166 | ||
04454e1e FG |
167 | /// Returns `Option` containing inner type if there is one. |
168 | pub(crate) fn inner_type(&self) -> Option<&'ty Type> { | |
169 | match self { | |
170 | FieldInnerTy::Option(inner) | FieldInnerTy::Vec(inner) => Some(inner), | |
171 | FieldInnerTy::None => None, | |
172 | } | |
173 | } | |
174 | ||
175 | /// Surrounds `inner` with destructured wrapper type, exposing inner type as `binding`. | |
176 | pub(crate) fn with(&self, binding: impl ToTokens, inner: impl ToTokens) -> TokenStream { | |
177 | match self { | |
178 | FieldInnerTy::Option(..) => quote! { | |
179 | if let Some(#binding) = #binding { | |
180 | #inner | |
181 | } | |
182 | }, | |
183 | FieldInnerTy::Vec(..) => quote! { | |
184 | for #binding in #binding { | |
185 | #inner | |
186 | } | |
187 | }, | |
188 | FieldInnerTy::None => quote! { #inner }, | |
189 | } | |
190 | } | |
191 | } | |
192 | ||
193 | /// Field information passed to the builder. Deliberately omits attrs to discourage the | |
194 | /// `generate_*` methods from walking the attributes themselves. | |
195 | pub(crate) struct FieldInfo<'a> { | |
04454e1e FG |
196 | pub(crate) binding: &'a BindingInfo<'a>, |
197 | pub(crate) ty: &'a Type, | |
198 | pub(crate) span: &'a proc_macro2::Span, | |
199 | } | |
200 | ||
201 | /// Small helper trait for abstracting over `Option` fields that contain a value and a `Span` | |
202 | /// for error reporting if they are set more than once. | |
203 | pub(crate) trait SetOnce<T> { | |
2b03887a | 204 | fn set_once(&mut self, value: T, span: Span); |
064997fb FG |
205 | |
206 | fn value(self) -> Option<T>; | |
2b03887a | 207 | fn value_ref(&self) -> Option<&T>; |
04454e1e FG |
208 | } |
209 | ||
2b03887a FG |
210 | /// An [`Option<T>`] that keeps track of the span that caused it to be set; used with [`SetOnce`]. |
211 | pub(super) type SpannedOption<T> = Option<(T, Span)>; | |
212 | ||
213 | impl<T> SetOnce<T> for SpannedOption<T> { | |
214 | fn set_once(&mut self, value: T, span: Span) { | |
04454e1e FG |
215 | match self { |
216 | None => { | |
217 | *self = Some((value, span)); | |
218 | } | |
219 | Some((_, prev_span)) => { | |
220 | span_err(span, "specified multiple times") | |
221 | .span_note(*prev_span, "previously specified here") | |
222 | .emit(); | |
223 | } | |
224 | } | |
225 | } | |
064997fb FG |
226 | |
227 | fn value(self) -> Option<T> { | |
228 | self.map(|(v, _)| v) | |
229 | } | |
2b03887a FG |
230 | |
231 | fn value_ref(&self) -> Option<&T> { | |
232 | self.as_ref().map(|(v, _)| v) | |
233 | } | |
04454e1e FG |
234 | } |
235 | ||
2b03887a FG |
236 | pub(super) type FieldMap = HashMap<String, TokenStream>; |
237 | ||
04454e1e FG |
238 | pub(crate) trait HasFieldMap { |
239 | /// Returns the binding for the field with the given name, if it exists on the type. | |
240 | fn get_field_binding(&self, field: &String) -> Option<&TokenStream>; | |
241 | ||
242 | /// In the strings in the attributes supplied to this macro, we want callers to be able to | |
243 | /// reference fields in the format string. For example: | |
244 | /// | |
245 | /// ```ignore (not-usage-example) | |
246 | /// /// Suggest `==` when users wrote `===`. | |
247 | /// #[suggestion(slug = "parser-not-javascript-eq", code = "{lhs} == {rhs}")] | |
248 | /// struct NotJavaScriptEq { | |
249 | /// #[primary_span] | |
250 | /// span: Span, | |
251 | /// lhs: Ident, | |
252 | /// rhs: Ident, | |
253 | /// } | |
254 | /// ``` | |
255 | /// | |
256 | /// We want to automatically pick up that `{lhs}` refers `self.lhs` and `{rhs}` refers to | |
257 | /// `self.rhs`, then generate this call to `format!`: | |
258 | /// | |
259 | /// ```ignore (not-usage-example) | |
260 | /// format!("{lhs} == {rhs}", lhs = self.lhs, rhs = self.rhs) | |
261 | /// ``` | |
262 | /// | |
263 | /// This function builds the entire call to `format!`. | |
264 | fn build_format(&self, input: &str, span: proc_macro2::Span) -> TokenStream { | |
265 | // This set is used later to generate the final format string. To keep builds reproducible, | |
266 | // the iteration order needs to be deterministic, hence why we use a `BTreeSet` here | |
267 | // instead of a `HashSet`. | |
268 | let mut referenced_fields: BTreeSet<String> = BTreeSet::new(); | |
269 | ||
270 | // At this point, we can start parsing the format string. | |
271 | let mut it = input.chars().peekable(); | |
272 | ||
273 | // Once the start of a format string has been found, process the format string and spit out | |
274 | // the referenced fields. Leaves `it` sitting on the closing brace of the format string, so | |
275 | // the next call to `it.next()` retrieves the next character. | |
276 | while let Some(c) = it.next() { | |
f2b60f7d FG |
277 | if c != '{' { |
278 | continue; | |
279 | } | |
280 | if *it.peek().unwrap_or(&'\0') == '{' { | |
281 | assert_eq!(it.next().unwrap(), '{'); | |
282 | continue; | |
283 | } | |
284 | let mut eat_argument = || -> Option<String> { | |
285 | let mut result = String::new(); | |
286 | // Format specifiers look like: | |
287 | // | |
288 | // format := '{' [ argument ] [ ':' format_spec ] '}' . | |
289 | // | |
290 | // Therefore, we only need to eat until ':' or '}' to find the argument. | |
291 | while let Some(c) = it.next() { | |
292 | result.push(c); | |
293 | let next = *it.peek().unwrap_or(&'\0'); | |
294 | if next == '}' { | |
295 | break; | |
296 | } else if next == ':' { | |
297 | // Eat the ':' character. | |
298 | assert_eq!(it.next().unwrap(), ':'); | |
299 | break; | |
04454e1e | 300 | } |
04454e1e | 301 | } |
f2b60f7d FG |
302 | // Eat until (and including) the matching '}' |
303 | while it.next()? != '}' { | |
304 | continue; | |
305 | } | |
306 | Some(result) | |
307 | }; | |
308 | ||
309 | if let Some(referenced_field) = eat_argument() { | |
310 | referenced_fields.insert(referenced_field); | |
04454e1e FG |
311 | } |
312 | } | |
313 | ||
314 | // At this point, `referenced_fields` contains a set of the unique fields that were | |
315 | // referenced in the format string. Generate the corresponding "x = self.x" format | |
316 | // string parameters: | |
317 | let args = referenced_fields.into_iter().map(|field: String| { | |
318 | let field_ident = format_ident!("{}", field); | |
319 | let value = match self.get_field_binding(&field) { | |
320 | Some(value) => value.clone(), | |
321 | // This field doesn't exist. Emit a diagnostic. | |
322 | None => { | |
323 | span_err( | |
324 | span.unwrap(), | |
f25598a0 | 325 | &format!("`{field}` doesn't refer to a field on this type"), |
04454e1e FG |
326 | ) |
327 | .emit(); | |
328 | quote! { | |
329 | "{#field}" | |
330 | } | |
331 | } | |
332 | }; | |
333 | quote! { | |
334 | #field_ident = #value | |
335 | } | |
336 | }); | |
337 | quote! { | |
338 | format!(#input #(,#args)*) | |
339 | } | |
340 | } | |
341 | } | |
342 | ||
343 | /// `Applicability` of a suggestion - mirrors `rustc_errors::Applicability` - and used to represent | |
344 | /// the user's selection of applicability if specified in an attribute. | |
2b03887a | 345 | #[derive(Clone, Copy)] |
04454e1e FG |
346 | pub(crate) enum Applicability { |
347 | MachineApplicable, | |
348 | MaybeIncorrect, | |
349 | HasPlaceholders, | |
350 | Unspecified, | |
351 | } | |
352 | ||
353 | impl FromStr for Applicability { | |
354 | type Err = (); | |
355 | ||
356 | fn from_str(s: &str) -> Result<Self, Self::Err> { | |
357 | match s { | |
358 | "machine-applicable" => Ok(Applicability::MachineApplicable), | |
359 | "maybe-incorrect" => Ok(Applicability::MaybeIncorrect), | |
360 | "has-placeholders" => Ok(Applicability::HasPlaceholders), | |
361 | "unspecified" => Ok(Applicability::Unspecified), | |
362 | _ => Err(()), | |
363 | } | |
364 | } | |
365 | } | |
366 | ||
367 | impl quote::ToTokens for Applicability { | |
368 | fn to_tokens(&self, tokens: &mut TokenStream) { | |
369 | tokens.extend(match self { | |
370 | Applicability::MachineApplicable => { | |
371 | quote! { rustc_errors::Applicability::MachineApplicable } | |
372 | } | |
373 | Applicability::MaybeIncorrect => { | |
374 | quote! { rustc_errors::Applicability::MaybeIncorrect } | |
375 | } | |
376 | Applicability::HasPlaceholders => { | |
377 | quote! { rustc_errors::Applicability::HasPlaceholders } | |
378 | } | |
379 | Applicability::Unspecified => { | |
380 | quote! { rustc_errors::Applicability::Unspecified } | |
381 | } | |
382 | }); | |
383 | } | |
384 | } | |
064997fb FG |
385 | |
386 | /// Build the mapping of field names to fields. This allows attributes to peek values from | |
387 | /// other fields. | |
f25598a0 | 388 | pub(super) fn build_field_mapping(variant: &VariantInfo<'_>) -> HashMap<String, TokenStream> { |
2b03887a FG |
389 | let mut fields_map = FieldMap::new(); |
390 | for binding in variant.bindings() { | |
391 | if let Some(ident) = &binding.ast().ident { | |
392 | fields_map.insert(ident.to_string(), quote! { #binding }); | |
393 | } | |
394 | } | |
395 | fields_map | |
396 | } | |
397 | ||
398 | #[derive(Copy, Clone, Debug)] | |
399 | pub(super) enum AllowMultipleAlternatives { | |
400 | No, | |
401 | Yes, | |
402 | } | |
403 | ||
404 | /// Constructs the `format!()` invocation(s) necessary for a `#[suggestion*(code = "foo")]` or | |
405 | /// `#[suggestion*(code("foo", "bar"))]` attribute field | |
406 | pub(super) fn build_suggestion_code( | |
407 | code_field: &Ident, | |
408 | meta: &Meta, | |
409 | fields: &impl HasFieldMap, | |
410 | allow_multiple: AllowMultipleAlternatives, | |
411 | ) -> TokenStream { | |
412 | let values = match meta { | |
413 | // `code = "foo"` | |
414 | Meta::NameValue(MetaNameValue { lit: syn::Lit::Str(s), .. }) => vec![s], | |
415 | // `code("foo", "bar")` | |
416 | Meta::List(MetaList { nested, .. }) => { | |
417 | if let AllowMultipleAlternatives::No = allow_multiple { | |
418 | span_err( | |
419 | meta.span().unwrap(), | |
420 | "expected exactly one string literal for `code = ...`", | |
421 | ) | |
422 | .emit(); | |
423 | vec![] | |
424 | } else if nested.is_empty() { | |
425 | span_err( | |
426 | meta.span().unwrap(), | |
427 | "expected at least one string literal for `code(...)`", | |
428 | ) | |
429 | .emit(); | |
430 | vec![] | |
431 | } else { | |
432 | nested | |
433 | .into_iter() | |
434 | .filter_map(|item| { | |
435 | if let NestedMeta::Lit(syn::Lit::Str(s)) = item { | |
436 | Some(s) | |
437 | } else { | |
438 | span_err( | |
439 | item.span().unwrap(), | |
440 | "`code(...)` must contain only string literals", | |
441 | ) | |
442 | .emit(); | |
443 | None | |
444 | } | |
445 | }) | |
446 | .collect() | |
064997fb FG |
447 | } |
448 | } | |
2b03887a FG |
449 | _ => { |
450 | span_err( | |
451 | meta.span().unwrap(), | |
452 | r#"`code = "..."`/`code(...)` must contain only string literals"#, | |
453 | ) | |
454 | .emit(); | |
455 | vec![] | |
456 | } | |
457 | }; | |
458 | ||
459 | if let AllowMultipleAlternatives::Yes = allow_multiple { | |
460 | let formatted_strings: Vec<_> = values | |
461 | .into_iter() | |
462 | .map(|value| fields.build_format(&value.value(), value.span())) | |
463 | .collect(); | |
464 | quote! { let #code_field = [#(#formatted_strings),*].into_iter(); } | |
465 | } else if let [value] = values.as_slice() { | |
466 | let formatted_str = fields.build_format(&value.value(), value.span()); | |
467 | quote! { let #code_field = #formatted_str; } | |
468 | } else { | |
469 | // error handled previously | |
470 | quote! { let #code_field = String::new(); } | |
064997fb | 471 | } |
2b03887a | 472 | } |
064997fb | 473 | |
2b03887a | 474 | /// Possible styles for suggestion subdiagnostics. |
487cf647 | 475 | #[derive(Clone, Copy, PartialEq)] |
2b03887a | 476 | pub(super) enum SuggestionKind { |
2b03887a | 477 | Normal, |
2b03887a | 478 | Short, |
2b03887a | 479 | Hidden, |
2b03887a | 480 | Verbose, |
487cf647 | 481 | ToolOnly, |
2b03887a FG |
482 | } |
483 | ||
484 | impl FromStr for SuggestionKind { | |
485 | type Err = (); | |
486 | ||
487 | fn from_str(s: &str) -> Result<Self, Self::Err> { | |
488 | match s { | |
487cf647 FG |
489 | "normal" => Ok(SuggestionKind::Normal), |
490 | "short" => Ok(SuggestionKind::Short), | |
491 | "hidden" => Ok(SuggestionKind::Hidden), | |
492 | "verbose" => Ok(SuggestionKind::Verbose), | |
493 | "tool-only" => Ok(SuggestionKind::ToolOnly), | |
2b03887a FG |
494 | _ => Err(()), |
495 | } | |
496 | } | |
497 | } | |
498 | ||
487cf647 FG |
499 | impl fmt::Display for SuggestionKind { |
500 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { | |
501 | match self { | |
502 | SuggestionKind::Normal => write!(f, "normal"), | |
503 | SuggestionKind::Short => write!(f, "short"), | |
504 | SuggestionKind::Hidden => write!(f, "hidden"), | |
505 | SuggestionKind::Verbose => write!(f, "verbose"), | |
506 | SuggestionKind::ToolOnly => write!(f, "tool-only"), | |
507 | } | |
508 | } | |
509 | } | |
510 | ||
2b03887a FG |
511 | impl SuggestionKind { |
512 | pub fn to_suggestion_style(&self) -> TokenStream { | |
513 | match self { | |
514 | SuggestionKind::Normal => { | |
515 | quote! { rustc_errors::SuggestionStyle::ShowCode } | |
516 | } | |
517 | SuggestionKind::Short => { | |
518 | quote! { rustc_errors::SuggestionStyle::HideCodeInline } | |
519 | } | |
520 | SuggestionKind::Hidden => { | |
521 | quote! { rustc_errors::SuggestionStyle::HideCodeAlways } | |
522 | } | |
523 | SuggestionKind::Verbose => { | |
524 | quote! { rustc_errors::SuggestionStyle::ShowAlways } | |
525 | } | |
487cf647 FG |
526 | SuggestionKind::ToolOnly => { |
527 | quote! { rustc_errors::SuggestionStyle::CompletelyHidden } | |
528 | } | |
529 | } | |
530 | } | |
531 | ||
532 | fn from_suffix(s: &str) -> Option<Self> { | |
533 | match s { | |
534 | "" => Some(SuggestionKind::Normal), | |
535 | "_short" => Some(SuggestionKind::Short), | |
536 | "_hidden" => Some(SuggestionKind::Hidden), | |
537 | "_verbose" => Some(SuggestionKind::Verbose), | |
538 | _ => None, | |
2b03887a FG |
539 | } |
540 | } | |
541 | } | |
542 | ||
543 | /// Types of subdiagnostics that can be created using attributes | |
544 | #[derive(Clone)] | |
545 | pub(super) enum SubdiagnosticKind { | |
546 | /// `#[label(...)]` | |
547 | Label, | |
548 | /// `#[note(...)]` | |
549 | Note, | |
550 | /// `#[help(...)]` | |
551 | Help, | |
552 | /// `#[warning(...)]` | |
553 | Warn, | |
554 | /// `#[suggestion{,_short,_hidden,_verbose}]` | |
555 | Suggestion { | |
556 | suggestion_kind: SuggestionKind, | |
557 | applicability: SpannedOption<Applicability>, | |
558 | /// Identifier for variable used for formatted code, e.g. `___code_0`. Enables separation | |
559 | /// of formatting and diagnostic emission so that `set_arg` calls can happen in-between.. | |
560 | code_field: syn::Ident, | |
561 | /// Initialization logic for `code_field`'s variable, e.g. | |
562 | /// `let __formatted_code = /* whatever */;` | |
563 | code_init: TokenStream, | |
564 | }, | |
565 | /// `#[multipart_suggestion{,_short,_hidden,_verbose}]` | |
566 | MultipartSuggestion { | |
567 | suggestion_kind: SuggestionKind, | |
568 | applicability: SpannedOption<Applicability>, | |
569 | }, | |
570 | } | |
571 | ||
572 | impl SubdiagnosticKind { | |
573 | /// Constructs a `SubdiagnosticKind` from a field or type attribute such as `#[note]`, | |
574 | /// `#[error(parser::add_paren)]` or `#[suggestion(code = "...")]`. Returns the | |
575 | /// `SubdiagnosticKind` and the diagnostic slug, if specified. | |
576 | pub(super) fn from_attr( | |
577 | attr: &Attribute, | |
578 | fields: &impl HasFieldMap, | |
579 | ) -> Result<Option<(SubdiagnosticKind, Option<Path>)>, DiagnosticDeriveError> { | |
580 | // Always allow documentation comments. | |
581 | if is_doc_comment(attr) { | |
582 | return Ok(None); | |
583 | } | |
584 | ||
585 | let span = attr.span().unwrap(); | |
586 | ||
587 | let name = attr.path.segments.last().unwrap().ident.to_string(); | |
588 | let name = name.as_str(); | |
589 | ||
590 | let meta = attr.parse_meta()?; | |
487cf647 | 591 | |
2b03887a FG |
592 | let mut kind = match name { |
593 | "label" => SubdiagnosticKind::Label, | |
594 | "note" => SubdiagnosticKind::Note, | |
595 | "help" => SubdiagnosticKind::Help, | |
596 | "warning" => SubdiagnosticKind::Warn, | |
597 | _ => { | |
487cf647 FG |
598 | // Recover old `#[(multipart_)suggestion_*]` syntaxes |
599 | // FIXME(#100717): remove | |
2b03887a | 600 | if let Some(suggestion_kind) = |
487cf647 | 601 | name.strip_prefix("suggestion").and_then(SuggestionKind::from_suffix) |
2b03887a | 602 | { |
487cf647 FG |
603 | if suggestion_kind != SuggestionKind::Normal { |
604 | invalid_attr(attr, &meta) | |
605 | .help(format!( | |
f25598a0 | 606 | r#"Use `#[suggestion(..., style = "{suggestion_kind}")]` instead"# |
487cf647 FG |
607 | )) |
608 | .emit(); | |
609 | } | |
610 | ||
2b03887a | 611 | SubdiagnosticKind::Suggestion { |
487cf647 | 612 | suggestion_kind: SuggestionKind::Normal, |
2b03887a FG |
613 | applicability: None, |
614 | code_field: new_code_ident(), | |
615 | code_init: TokenStream::new(), | |
616 | } | |
617 | } else if let Some(suggestion_kind) = | |
487cf647 | 618 | name.strip_prefix("multipart_suggestion").and_then(SuggestionKind::from_suffix) |
2b03887a | 619 | { |
487cf647 FG |
620 | if suggestion_kind != SuggestionKind::Normal { |
621 | invalid_attr(attr, &meta) | |
622 | .help(format!( | |
f25598a0 | 623 | r#"Use `#[multipart_suggestion(..., style = "{suggestion_kind}")]` instead"# |
487cf647 FG |
624 | )) |
625 | .emit(); | |
626 | } | |
627 | ||
628 | SubdiagnosticKind::MultipartSuggestion { | |
629 | suggestion_kind: SuggestionKind::Normal, | |
630 | applicability: None, | |
631 | } | |
2b03887a FG |
632 | } else { |
633 | throw_invalid_attr!(attr, &meta); | |
634 | } | |
635 | } | |
636 | }; | |
637 | ||
638 | let nested = match meta { | |
639 | Meta::List(MetaList { ref nested, .. }) => { | |
640 | // An attribute with properties, such as `#[suggestion(code = "...")]` or | |
641 | // `#[error(some::slug)]` | |
642 | nested | |
643 | } | |
644 | Meta::Path(_) => { | |
645 | // An attribute without a slug or other properties, such as `#[note]` - return | |
646 | // without further processing. | |
647 | // | |
648 | // Only allow this if there are no mandatory properties, such as `code = "..."` in | |
649 | // `#[suggestion(...)]` | |
650 | match kind { | |
651 | SubdiagnosticKind::Label | |
652 | | SubdiagnosticKind::Note | |
653 | | SubdiagnosticKind::Help | |
654 | | SubdiagnosticKind::Warn | |
655 | | SubdiagnosticKind::MultipartSuggestion { .. } => { | |
656 | return Ok(Some((kind, None))); | |
657 | } | |
658 | SubdiagnosticKind::Suggestion { .. } => { | |
659 | throw_span_err!(span, "suggestion without `code = \"...\"`") | |
660 | } | |
661 | } | |
662 | } | |
663 | _ => { | |
664 | throw_invalid_attr!(attr, &meta) | |
665 | } | |
666 | }; | |
667 | ||
668 | let mut code = None; | |
487cf647 | 669 | let mut suggestion_kind = None; |
2b03887a FG |
670 | |
671 | let mut nested_iter = nested.into_iter().peekable(); | |
672 | ||
673 | // Peek at the first nested attribute: if it's a slug path, consume it. | |
674 | let slug = if let Some(NestedMeta::Meta(Meta::Path(path))) = nested_iter.peek() { | |
675 | let path = path.clone(); | |
676 | // Advance the iterator. | |
677 | nested_iter.next(); | |
678 | Some(path) | |
679 | } else { | |
680 | None | |
681 | }; | |
682 | ||
683 | for nested_attr in nested_iter { | |
684 | let meta = match nested_attr { | |
685 | NestedMeta::Meta(ref meta) => meta, | |
686 | NestedMeta::Lit(_) => { | |
487cf647 | 687 | invalid_nested_attr(attr, nested_attr).emit(); |
2b03887a FG |
688 | continue; |
689 | } | |
690 | }; | |
691 | ||
692 | let span = meta.span().unwrap(); | |
693 | let nested_name = meta.path().segments.last().unwrap().ident.to_string(); | |
694 | let nested_name = nested_name.as_str(); | |
695 | ||
696 | let string_value = match meta { | |
697 | Meta::NameValue(MetaNameValue { lit: syn::Lit::Str(value), .. }) => Some(value), | |
698 | ||
487cf647 | 699 | Meta::Path(_) => throw_invalid_nested_attr!(attr, nested_attr, |diag| { |
2b03887a FG |
700 | diag.help("a diagnostic slug must be the first argument to the attribute") |
701 | }), | |
702 | _ => None, | |
703 | }; | |
704 | ||
705 | match (nested_name, &mut kind) { | |
706 | ("code", SubdiagnosticKind::Suggestion { code_field, .. }) => { | |
707 | let code_init = build_suggestion_code( | |
708 | code_field, | |
709 | meta, | |
710 | fields, | |
711 | AllowMultipleAlternatives::Yes, | |
712 | ); | |
713 | code.set_once(code_init, span); | |
714 | } | |
715 | ( | |
716 | "applicability", | |
717 | SubdiagnosticKind::Suggestion { ref mut applicability, .. } | |
718 | | SubdiagnosticKind::MultipartSuggestion { ref mut applicability, .. }, | |
719 | ) => { | |
720 | let Some(value) = string_value else { | |
487cf647 | 721 | invalid_nested_attr(attr, nested_attr).emit(); |
2b03887a FG |
722 | continue; |
723 | }; | |
724 | ||
725 | let value = Applicability::from_str(&value.value()).unwrap_or_else(|()| { | |
726 | span_err(span, "invalid applicability").emit(); | |
727 | Applicability::Unspecified | |
728 | }); | |
729 | applicability.set_once(value, span); | |
730 | } | |
487cf647 FG |
731 | ( |
732 | "style", | |
733 | SubdiagnosticKind::Suggestion { .. } | |
734 | | SubdiagnosticKind::MultipartSuggestion { .. }, | |
735 | ) => { | |
736 | let Some(value) = string_value else { | |
737 | invalid_nested_attr(attr, nested_attr).emit(); | |
738 | continue; | |
739 | }; | |
740 | ||
741 | let value = value.value().parse().unwrap_or_else(|()| { | |
742 | span_err(value.span().unwrap(), "invalid suggestion style") | |
743 | .help("valid styles are `normal`, `short`, `hidden`, `verbose` and `tool-only`") | |
744 | .emit(); | |
745 | SuggestionKind::Normal | |
746 | }); | |
747 | ||
748 | suggestion_kind.set_once(value, span); | |
749 | } | |
2b03887a FG |
750 | |
751 | // Invalid nested attribute | |
752 | (_, SubdiagnosticKind::Suggestion { .. }) => { | |
487cf647 FG |
753 | invalid_nested_attr(attr, nested_attr) |
754 | .help( | |
755 | "only `style`, `code` and `applicability` are valid nested attributes", | |
756 | ) | |
2b03887a FG |
757 | .emit(); |
758 | } | |
759 | (_, SubdiagnosticKind::MultipartSuggestion { .. }) => { | |
487cf647 FG |
760 | invalid_nested_attr(attr, nested_attr) |
761 | .help("only `style` and `applicability` are valid nested attributes") | |
2b03887a FG |
762 | .emit() |
763 | } | |
764 | _ => { | |
487cf647 | 765 | invalid_nested_attr(attr, nested_attr).emit(); |
2b03887a FG |
766 | } |
767 | } | |
768 | } | |
769 | ||
770 | match kind { | |
487cf647 FG |
771 | SubdiagnosticKind::Suggestion { |
772 | ref code_field, | |
773 | ref mut code_init, | |
774 | suggestion_kind: ref mut kind_field, | |
775 | .. | |
776 | } => { | |
777 | if let Some(kind) = suggestion_kind.value() { | |
778 | *kind_field = kind; | |
779 | } | |
780 | ||
2b03887a FG |
781 | *code_init = if let Some(init) = code.value() { |
782 | init | |
783 | } else { | |
784 | span_err(span, "suggestion without `code = \"...\"`").emit(); | |
785 | quote! { let #code_field = std::iter::empty(); } | |
786 | }; | |
787 | } | |
487cf647 FG |
788 | SubdiagnosticKind::MultipartSuggestion { |
789 | suggestion_kind: ref mut kind_field, .. | |
790 | } => { | |
791 | if let Some(kind) = suggestion_kind.value() { | |
792 | *kind_field = kind; | |
793 | } | |
794 | } | |
2b03887a FG |
795 | SubdiagnosticKind::Label |
796 | | SubdiagnosticKind::Note | |
797 | | SubdiagnosticKind::Help | |
487cf647 | 798 | | SubdiagnosticKind::Warn => {} |
2b03887a FG |
799 | } |
800 | ||
801 | Ok(Some((kind, slug))) | |
802 | } | |
803 | } | |
804 | ||
805 | impl quote::IdentFragment for SubdiagnosticKind { | |
806 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { | |
807 | match self { | |
808 | SubdiagnosticKind::Label => write!(f, "label"), | |
809 | SubdiagnosticKind::Note => write!(f, "note"), | |
810 | SubdiagnosticKind::Help => write!(f, "help"), | |
811 | SubdiagnosticKind::Warn => write!(f, "warn"), | |
812 | SubdiagnosticKind::Suggestion { .. } => write!(f, "suggestions_with_style"), | |
813 | SubdiagnosticKind::MultipartSuggestion { .. } => { | |
814 | write!(f, "multipart_suggestion_with_style") | |
815 | } | |
816 | } | |
817 | } | |
818 | ||
819 | fn span(&self) -> Option<proc_macro2::Span> { | |
820 | None | |
821 | } | |
822 | } | |
823 | ||
824 | /// Returns `true` if `field` should generate a `set_arg` call rather than any other diagnostic | |
825 | /// call (like `span_label`). | |
826 | pub(super) fn should_generate_set_arg(field: &Field) -> bool { | |
827 | field.attrs.is_empty() | |
828 | } | |
829 | ||
830 | pub(super) fn is_doc_comment(attr: &Attribute) -> bool { | |
487cf647 | 831 | attr.path.segments.last().unwrap().ident == "doc" |
064997fb | 832 | } |