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