]>
Commit | Line | Data |
---|---|---|
f20569fa XL |
1 | //! Format attributes and meta items. |
2 | ||
3 | use rustc_ast::ast; | |
cdc7bbd5 | 4 | use rustc_ast::AstLike; |
f20569fa XL |
5 | use rustc_span::{symbol::sym, Span, Symbol}; |
6 | ||
7 | use self::doc_comment::DocCommentFormatter; | |
8 | use crate::comment::{contains_comment, rewrite_doc_comment, CommentStyle}; | |
9 | use crate::config::lists::*; | |
10 | use crate::config::IndentStyle; | |
11 | use crate::expr::rewrite_literal; | |
12 | use crate::lists::{definitive_tactic, itemize_list, write_list, ListFormatting, Separator}; | |
13 | use crate::overflow; | |
14 | use crate::rewrite::{Rewrite, RewriteContext}; | |
15 | use crate::shape::Shape; | |
16 | use crate::types::{rewrite_path, PathContext}; | |
17 | use crate::utils::{count_newlines, mk_sp}; | |
18 | ||
19 | mod doc_comment; | |
20 | ||
21 | pub(crate) fn contains_name(attrs: &[ast::Attribute], name: Symbol) -> bool { | |
22 | attrs.iter().any(|attr| attr.has_name(name)) | |
23 | } | |
24 | ||
25 | pub(crate) fn first_attr_value_str_by_name( | |
26 | attrs: &[ast::Attribute], | |
27 | name: Symbol, | |
28 | ) -> Option<Symbol> { | |
29 | attrs | |
30 | .iter() | |
31 | .find(|attr| attr.has_name(name)) | |
32 | .and_then(|attr| attr.value_str()) | |
33 | } | |
34 | ||
35 | /// Returns attributes on the given statement. | |
36 | pub(crate) fn get_attrs_from_stmt(stmt: &ast::Stmt) -> &[ast::Attribute] { | |
37 | stmt.attrs() | |
38 | } | |
39 | ||
40 | pub(crate) fn get_span_without_attrs(stmt: &ast::Stmt) -> Span { | |
41 | match stmt.kind { | |
42 | ast::StmtKind::Local(ref local) => local.span, | |
43 | ast::StmtKind::Item(ref item) => item.span, | |
44 | ast::StmtKind::Expr(ref expr) | ast::StmtKind::Semi(ref expr) => expr.span, | |
45 | ast::StmtKind::MacCall(ref mac_stmt) => mac_stmt.mac.span(), | |
46 | ast::StmtKind::Empty => stmt.span, | |
47 | } | |
48 | } | |
49 | ||
50 | /// Returns attributes that are within `outer_span`. | |
51 | pub(crate) fn filter_inline_attrs( | |
52 | attrs: &[ast::Attribute], | |
53 | outer_span: Span, | |
54 | ) -> Vec<ast::Attribute> { | |
55 | attrs | |
56 | .iter() | |
57 | .filter(|a| outer_span.lo() <= a.span.lo() && a.span.hi() <= outer_span.hi()) | |
58 | .cloned() | |
59 | .collect() | |
60 | } | |
61 | ||
62 | fn is_derive(attr: &ast::Attribute) -> bool { | |
63 | attr.has_name(sym::derive) | |
64 | } | |
65 | ||
66 | // The shape of the arguments to a function-like attribute. | |
67 | fn argument_shape( | |
68 | left: usize, | |
69 | right: usize, | |
70 | combine: bool, | |
71 | shape: Shape, | |
72 | context: &RewriteContext<'_>, | |
73 | ) -> Option<Shape> { | |
74 | match context.config.indent_style() { | |
75 | IndentStyle::Block => { | |
76 | if combine { | |
77 | shape.offset_left(left) | |
78 | } else { | |
79 | Some( | |
80 | shape | |
81 | .block_indent(context.config.tab_spaces()) | |
82 | .with_max_width(context.config), | |
83 | ) | |
84 | } | |
85 | } | |
86 | IndentStyle::Visual => shape | |
87 | .visual_indent(0) | |
88 | .shrink_left(left) | |
89 | .and_then(|s| s.sub_width(right)), | |
90 | } | |
91 | } | |
92 | ||
93 | fn format_derive( | |
94 | derives: &[ast::Attribute], | |
95 | shape: Shape, | |
96 | context: &RewriteContext<'_>, | |
97 | ) -> Option<String> { | |
98 | // Collect all items from all attributes | |
99 | let all_items = derives | |
100 | .iter() | |
101 | .map(|attr| { | |
102 | // Parse the derive items and extract the span for each item; if any | |
103 | // attribute is not parseable, none of the attributes will be | |
104 | // reformatted. | |
105 | let item_spans = attr.meta_item_list().map(|meta_item_list| { | |
106 | meta_item_list | |
107 | .into_iter() | |
108 | .map(|nested_meta_item| nested_meta_item.span()) | |
109 | })?; | |
110 | ||
111 | let items = itemize_list( | |
112 | context.snippet_provider, | |
113 | item_spans, | |
114 | ")", | |
115 | ",", | |
116 | |span| span.lo(), | |
117 | |span| span.hi(), | |
118 | |span| Some(context.snippet(*span).to_owned()), | |
119 | attr.span.lo(), | |
120 | attr.span.hi(), | |
121 | false, | |
122 | ); | |
123 | ||
124 | Some(items) | |
125 | }) | |
126 | // Fail if any attribute failed. | |
127 | .collect::<Option<Vec<_>>>()? | |
128 | // Collect the results into a single, flat, Vec. | |
129 | .into_iter() | |
130 | .flatten() | |
131 | .collect::<Vec<_>>(); | |
132 | ||
133 | // Collect formatting parameters. | |
134 | let prefix = attr_prefix(&derives[0]); | |
135 | let argument_shape = argument_shape( | |
136 | "[derive()]".len() + prefix.len(), | |
137 | ")]".len(), | |
138 | false, | |
139 | shape, | |
140 | context, | |
141 | )?; | |
142 | let one_line_shape = shape | |
143 | .offset_left("[derive()]".len() + prefix.len())? | |
144 | .sub_width("()]".len())?; | |
145 | let one_line_budget = one_line_shape.width; | |
146 | ||
147 | let tactic = definitive_tactic( | |
148 | &all_items, | |
149 | ListTactic::HorizontalVertical, | |
150 | Separator::Comma, | |
151 | argument_shape.width, | |
152 | ); | |
153 | let trailing_separator = match context.config.indent_style() { | |
154 | // We always add the trailing comma and remove it if it is not needed. | |
155 | IndentStyle::Block => SeparatorTactic::Always, | |
156 | IndentStyle::Visual => SeparatorTactic::Never, | |
157 | }; | |
158 | ||
159 | // Format the collection of items. | |
160 | let fmt = ListFormatting::new(argument_shape, context.config) | |
161 | .tactic(tactic) | |
162 | .trailing_separator(trailing_separator) | |
163 | .ends_with_newline(false); | |
164 | let item_str = write_list(&all_items, &fmt)?; | |
165 | ||
166 | debug!("item_str: '{}'", item_str); | |
167 | ||
168 | // Determine if the result will be nested, i.e. if we're using the block | |
169 | // indent style and either the items are on multiple lines or we've exceeded | |
170 | // our budget to fit on a single line. | |
171 | let nested = context.config.indent_style() == IndentStyle::Block | |
172 | && (item_str.contains('\n') || item_str.len() > one_line_budget); | |
173 | ||
174 | // Format the final result. | |
175 | let mut result = String::with_capacity(128); | |
176 | result.push_str(prefix); | |
177 | result.push_str("[derive("); | |
178 | if nested { | |
179 | let nested_indent = argument_shape.indent.to_string_with_newline(context.config); | |
180 | result.push_str(&nested_indent); | |
181 | result.push_str(&item_str); | |
182 | result.push_str(&shape.indent.to_string_with_newline(context.config)); | |
183 | } else if let SeparatorTactic::Always = context.config.trailing_comma() { | |
184 | // Retain the trailing comma. | |
185 | result.push_str(&item_str); | |
94222f64 | 186 | } else if item_str.ends_with(',') { |
f20569fa XL |
187 | // Remove the trailing comma. |
188 | result.push_str(&item_str[..item_str.len() - 1]); | |
189 | } else { | |
190 | result.push_str(&item_str); | |
191 | } | |
192 | result.push_str(")]"); | |
193 | ||
194 | Some(result) | |
195 | } | |
196 | ||
197 | /// Returns the first group of attributes that fills the given predicate. | |
198 | /// We consider two doc comments are in different group if they are separated by normal comments. | |
199 | fn take_while_with_pred<'a, P>( | |
200 | context: &RewriteContext<'_>, | |
201 | attrs: &'a [ast::Attribute], | |
202 | pred: P, | |
203 | ) -> &'a [ast::Attribute] | |
204 | where | |
205 | P: Fn(&ast::Attribute) -> bool, | |
206 | { | |
207 | let mut len = 0; | |
208 | let mut iter = attrs.iter().peekable(); | |
209 | ||
210 | while let Some(attr) = iter.next() { | |
211 | if pred(attr) { | |
212 | len += 1; | |
213 | } else { | |
214 | break; | |
215 | } | |
216 | if let Some(next_attr) = iter.peek() { | |
217 | // Extract comments between two attributes. | |
218 | let span_between_attr = mk_sp(attr.span.hi(), next_attr.span.lo()); | |
219 | let snippet = context.snippet(span_between_attr); | |
220 | if count_newlines(snippet) >= 2 || snippet.contains('/') { | |
221 | break; | |
222 | } | |
223 | } | |
224 | } | |
225 | ||
226 | &attrs[..len] | |
227 | } | |
228 | ||
229 | /// Rewrite the any doc comments which come before any other attributes. | |
230 | fn rewrite_initial_doc_comments( | |
231 | context: &RewriteContext<'_>, | |
232 | attrs: &[ast::Attribute], | |
233 | shape: Shape, | |
234 | ) -> Option<(usize, Option<String>)> { | |
235 | if attrs.is_empty() { | |
236 | return Some((0, None)); | |
237 | } | |
238 | // Rewrite doc comments | |
239 | let sugared_docs = take_while_with_pred(context, attrs, |a| a.is_doc_comment()); | |
240 | if !sugared_docs.is_empty() { | |
241 | let snippet = sugared_docs | |
242 | .iter() | |
243 | .map(|a| context.snippet(a.span)) | |
244 | .collect::<Vec<_>>() | |
245 | .join("\n"); | |
246 | return Some(( | |
247 | sugared_docs.len(), | |
248 | Some(rewrite_doc_comment( | |
249 | &snippet, | |
250 | shape.comment(context.config), | |
251 | context.config, | |
252 | )?), | |
253 | )); | |
254 | } | |
255 | ||
256 | Some((0, None)) | |
257 | } | |
258 | ||
259 | impl Rewrite for ast::NestedMetaItem { | |
260 | fn rewrite(&self, context: &RewriteContext<'_>, shape: Shape) -> Option<String> { | |
261 | match self { | |
262 | ast::NestedMetaItem::MetaItem(ref meta_item) => meta_item.rewrite(context, shape), | |
263 | ast::NestedMetaItem::Literal(ref l) => rewrite_literal(context, l, shape), | |
264 | } | |
265 | } | |
266 | } | |
267 | ||
268 | fn has_newlines_before_after_comment(comment: &str) -> (&str, &str) { | |
269 | // Look at before and after comment and see if there are any empty lines. | |
270 | let comment_begin = comment.find('/'); | |
271 | let len = comment_begin.unwrap_or_else(|| comment.len()); | |
272 | let mlb = count_newlines(&comment[..len]) > 1; | |
273 | let mla = if comment_begin.is_none() { | |
274 | mlb | |
275 | } else { | |
276 | comment | |
277 | .chars() | |
278 | .rev() | |
279 | .take_while(|c| c.is_whitespace()) | |
280 | .filter(|&c| c == '\n') | |
281 | .count() | |
282 | > 1 | |
283 | }; | |
284 | (if mlb { "\n" } else { "" }, if mla { "\n" } else { "" }) | |
285 | } | |
286 | ||
287 | impl Rewrite for ast::MetaItem { | |
288 | fn rewrite(&self, context: &RewriteContext<'_>, shape: Shape) -> Option<String> { | |
289 | Some(match self.kind { | |
290 | ast::MetaItemKind::Word => { | |
291 | rewrite_path(context, PathContext::Type, None, &self.path, shape)? | |
292 | } | |
293 | ast::MetaItemKind::List(ref list) => { | |
294 | let path = rewrite_path(context, PathContext::Type, None, &self.path, shape)?; | |
295 | let has_trailing_comma = crate::expr::span_ends_with_comma(context, self.span); | |
296 | overflow::rewrite_with_parens( | |
297 | context, | |
298 | &path, | |
299 | list.iter(), | |
300 | // 1 = "]" | |
301 | shape.sub_width(1)?, | |
302 | self.span, | |
cdc7bbd5 | 303 | context.config.attr_fn_like_width(), |
f20569fa XL |
304 | Some(if has_trailing_comma { |
305 | SeparatorTactic::Always | |
306 | } else { | |
307 | SeparatorTactic::Never | |
308 | }), | |
309 | )? | |
310 | } | |
311 | ast::MetaItemKind::NameValue(ref literal) => { | |
312 | let path = rewrite_path(context, PathContext::Type, None, &self.path, shape)?; | |
313 | // 3 = ` = ` | |
314 | let lit_shape = shape.shrink_left(path.len() + 3)?; | |
315 | // `rewrite_literal` returns `None` when `literal` exceeds max | |
316 | // width. Since a literal is basically unformattable unless it | |
317 | // is a string literal (and only if `format_strings` is set), | |
318 | // we might be better off ignoring the fact that the attribute | |
319 | // is longer than the max width and continue on formatting. | |
320 | // See #2479 for example. | |
321 | let value = rewrite_literal(context, literal, lit_shape) | |
322 | .unwrap_or_else(|| context.snippet(literal.span).to_owned()); | |
323 | format!("{} = {}", path, value) | |
324 | } | |
325 | }) | |
326 | } | |
327 | } | |
328 | ||
329 | impl Rewrite for ast::Attribute { | |
330 | fn rewrite(&self, context: &RewriteContext<'_>, shape: Shape) -> Option<String> { | |
331 | let snippet = context.snippet(self.span); | |
332 | if self.is_doc_comment() { | |
333 | rewrite_doc_comment(snippet, shape.comment(context.config), context.config) | |
334 | } else { | |
335 | let should_skip = self | |
336 | .ident() | |
337 | .map(|s| context.skip_context.skip_attribute(&s.name.as_str())) | |
338 | .unwrap_or(false); | |
339 | let prefix = attr_prefix(self); | |
340 | ||
341 | if should_skip || contains_comment(snippet) { | |
342 | return Some(snippet.to_owned()); | |
343 | } | |
344 | ||
345 | if let Some(ref meta) = self.meta() { | |
346 | // This attribute is possibly a doc attribute needing normalization to a doc comment | |
347 | if context.config.normalize_doc_attributes() && meta.has_name(sym::doc) { | |
348 | if let Some(ref literal) = meta.value_str() { | |
349 | let comment_style = match self.style { | |
350 | ast::AttrStyle::Inner => CommentStyle::Doc, | |
351 | ast::AttrStyle::Outer => CommentStyle::TripleSlash, | |
352 | }; | |
353 | ||
354 | let literal_str = literal.as_str(); | |
355 | let doc_comment_formatter = | |
356 | DocCommentFormatter::new(&*literal_str, comment_style); | |
357 | let doc_comment = format!("{}", doc_comment_formatter); | |
358 | return rewrite_doc_comment( | |
359 | &doc_comment, | |
360 | shape.comment(context.config), | |
361 | context.config, | |
362 | ); | |
363 | } | |
364 | } | |
365 | ||
366 | // 1 = `[` | |
367 | let shape = shape.offset_left(prefix.len() + 1)?; | |
368 | Some( | |
369 | meta.rewrite(context, shape) | |
370 | .map_or_else(|| snippet.to_owned(), |rw| format!("{}[{}]", prefix, rw)), | |
371 | ) | |
372 | } else { | |
373 | Some(snippet.to_owned()) | |
374 | } | |
375 | } | |
376 | } | |
377 | } | |
378 | ||
cdc7bbd5 | 379 | impl Rewrite for [ast::Attribute] { |
f20569fa XL |
380 | fn rewrite(&self, context: &RewriteContext<'_>, shape: Shape) -> Option<String> { |
381 | if self.is_empty() { | |
382 | return Some(String::new()); | |
383 | } | |
384 | ||
385 | // The current remaining attributes. | |
386 | let mut attrs = self; | |
387 | let mut result = String::new(); | |
388 | ||
389 | // This is not just a simple map because we need to handle doc comments | |
390 | // (where we take as many doc comment attributes as possible) and possibly | |
391 | // merging derives into a single attribute. | |
392 | loop { | |
393 | if attrs.is_empty() { | |
394 | return Some(result); | |
395 | } | |
396 | ||
397 | // Handle doc comments. | |
398 | let (doc_comment_len, doc_comment_str) = | |
399 | rewrite_initial_doc_comments(context, attrs, shape)?; | |
400 | if doc_comment_len > 0 { | |
401 | let doc_comment_str = doc_comment_str.expect("doc comments, but no result"); | |
402 | result.push_str(&doc_comment_str); | |
403 | ||
404 | let missing_span = attrs | |
405 | .get(doc_comment_len) | |
406 | .map(|next| mk_sp(attrs[doc_comment_len - 1].span.hi(), next.span.lo())); | |
407 | if let Some(missing_span) = missing_span { | |
408 | let snippet = context.snippet(missing_span); | |
409 | let (mla, mlb) = has_newlines_before_after_comment(snippet); | |
410 | let comment = crate::comment::recover_missing_comment_in_span( | |
411 | missing_span, | |
412 | shape.with_max_width(context.config), | |
413 | context, | |
414 | 0, | |
415 | )?; | |
416 | let comment = if comment.is_empty() { | |
417 | format!("\n{}", mlb) | |
418 | } else { | |
419 | format!("{}{}\n{}", mla, comment, mlb) | |
420 | }; | |
421 | result.push_str(&comment); | |
422 | result.push_str(&shape.indent.to_string(context.config)); | |
423 | } | |
424 | ||
425 | attrs = &attrs[doc_comment_len..]; | |
426 | ||
427 | continue; | |
428 | } | |
429 | ||
430 | // Handle derives if we will merge them. | |
431 | if context.config.merge_derives() && is_derive(&attrs[0]) { | |
432 | let derives = take_while_with_pred(context, attrs, is_derive); | |
433 | let derive_str = format_derive(derives, shape, context)?; | |
434 | result.push_str(&derive_str); | |
435 | ||
436 | let missing_span = attrs | |
437 | .get(derives.len()) | |
438 | .map(|next| mk_sp(attrs[derives.len() - 1].span.hi(), next.span.lo())); | |
439 | if let Some(missing_span) = missing_span { | |
440 | let comment = crate::comment::recover_missing_comment_in_span( | |
441 | missing_span, | |
442 | shape.with_max_width(context.config), | |
443 | context, | |
444 | 0, | |
445 | )?; | |
446 | result.push_str(&comment); | |
447 | if let Some(next) = attrs.get(derives.len()) { | |
448 | if next.is_doc_comment() { | |
449 | let snippet = context.snippet(missing_span); | |
450 | let (_, mlb) = has_newlines_before_after_comment(snippet); | |
451 | result.push_str(&mlb); | |
452 | } | |
453 | } | |
454 | result.push('\n'); | |
455 | result.push_str(&shape.indent.to_string(context.config)); | |
456 | } | |
457 | ||
458 | attrs = &attrs[derives.len()..]; | |
459 | ||
460 | continue; | |
461 | } | |
462 | ||
463 | // If we get here, then we have a regular attribute, just handle one | |
464 | // at a time. | |
465 | ||
466 | let formatted_attr = attrs[0].rewrite(context, shape)?; | |
467 | result.push_str(&formatted_attr); | |
468 | ||
469 | let missing_span = attrs | |
470 | .get(1) | |
471 | .map(|next| mk_sp(attrs[0].span.hi(), next.span.lo())); | |
472 | if let Some(missing_span) = missing_span { | |
473 | let comment = crate::comment::recover_missing_comment_in_span( | |
474 | missing_span, | |
475 | shape.with_max_width(context.config), | |
476 | context, | |
477 | 0, | |
478 | )?; | |
479 | result.push_str(&comment); | |
480 | if let Some(next) = attrs.get(1) { | |
481 | if next.is_doc_comment() { | |
482 | let snippet = context.snippet(missing_span); | |
483 | let (_, mlb) = has_newlines_before_after_comment(snippet); | |
484 | result.push_str(&mlb); | |
485 | } | |
486 | } | |
487 | result.push('\n'); | |
488 | result.push_str(&shape.indent.to_string(context.config)); | |
489 | } | |
490 | ||
491 | attrs = &attrs[1..]; | |
492 | } | |
493 | } | |
494 | } | |
495 | ||
496 | fn attr_prefix(attr: &ast::Attribute) -> &'static str { | |
497 | match attr.style { | |
498 | ast::AttrStyle::Inner => "#!", | |
499 | ast::AttrStyle::Outer => "#", | |
500 | } | |
501 | } | |
502 | ||
503 | pub(crate) trait MetaVisitor<'ast> { | |
504 | fn visit_meta_item(&mut self, meta_item: &'ast ast::MetaItem) { | |
505 | match meta_item.kind { | |
506 | ast::MetaItemKind::Word => self.visit_meta_word(meta_item), | |
507 | ast::MetaItemKind::List(ref list) => self.visit_meta_list(meta_item, list), | |
508 | ast::MetaItemKind::NameValue(ref lit) => self.visit_meta_name_value(meta_item, lit), | |
509 | } | |
510 | } | |
511 | ||
512 | fn visit_meta_list( | |
513 | &mut self, | |
514 | _meta_item: &'ast ast::MetaItem, | |
515 | list: &'ast [ast::NestedMetaItem], | |
516 | ) { | |
517 | for nm in list { | |
518 | self.visit_nested_meta_item(nm); | |
519 | } | |
520 | } | |
521 | ||
522 | fn visit_meta_word(&mut self, _meta_item: &'ast ast::MetaItem) {} | |
523 | ||
524 | fn visit_meta_name_value(&mut self, _meta_item: &'ast ast::MetaItem, _lit: &'ast ast::Lit) {} | |
525 | ||
526 | fn visit_nested_meta_item(&mut self, nm: &'ast ast::NestedMetaItem) { | |
527 | match nm { | |
528 | ast::NestedMetaItem::MetaItem(ref meta_item) => self.visit_meta_item(meta_item), | |
529 | ast::NestedMetaItem::Literal(ref lit) => self.visit_literal(lit), | |
530 | } | |
531 | } | |
532 | ||
533 | fn visit_literal(&mut self, _lit: &'ast ast::Lit) {} | |
534 | } |