|
| 1 | +// Copyright 2018 The Rust Project Developers. See the COPYRIGHT |
| 2 | +// file at the top-level directory of this distribution and at |
| 3 | +// http://rust-lang.org/COPYRIGHT. |
| 4 | +// |
| 5 | +// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or |
| 6 | +// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license |
| 7 | +// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your |
| 8 | +// option. This file may not be copied, modified, or distributed |
| 9 | +// except according to those terms. |
| 10 | + |
| 11 | +//! Format attributes and meta items. |
| 12 | +
|
| 13 | +use config::lists::*; |
| 14 | +use syntax::ast; |
| 15 | +use syntax::codemap::Span; |
| 16 | + |
| 17 | +use comment::{combine_strs_with_missing_comments, contains_comment, rewrite_doc_comment}; |
| 18 | +use expr::rewrite_literal; |
| 19 | +use lists::{itemize_list, write_list, ListFormatting}; |
| 20 | +use rewrite::{Rewrite, RewriteContext}; |
| 21 | +use shape::Shape; |
| 22 | +use utils::{count_newlines, mk_sp}; |
| 23 | + |
| 24 | +use std::cmp; |
| 25 | + |
| 26 | +/// Returns attributes on the given statement. |
| 27 | +pub fn get_attrs_from_stmt(stmt: &ast::Stmt) -> &[ast::Attribute] { |
| 28 | + match stmt.node { |
| 29 | + ast::StmtKind::Local(ref local) => &local.attrs, |
| 30 | + ast::StmtKind::Item(ref item) => &item.attrs, |
| 31 | + ast::StmtKind::Expr(ref expr) | ast::StmtKind::Semi(ref expr) => &expr.attrs, |
| 32 | + ast::StmtKind::Mac(ref mac) => &mac.2, |
| 33 | + } |
| 34 | +} |
| 35 | + |
| 36 | +/// Returns attributes that are within `outer_span`. |
| 37 | +pub fn filter_inline_attrs(attrs: &[ast::Attribute], outer_span: Span) -> Vec<ast::Attribute> { |
| 38 | + attrs |
| 39 | + .iter() |
| 40 | + .filter(|a| outer_span.lo() <= a.span.lo() && a.span.hi() <= outer_span.hi()) |
| 41 | + .cloned() |
| 42 | + .collect() |
| 43 | +} |
| 44 | + |
| 45 | +fn is_derive(attr: &ast::Attribute) -> bool { |
| 46 | + attr.check_name("derive") |
| 47 | +} |
| 48 | + |
| 49 | +/// Returns the arguments of `#[derive(...)]`. |
| 50 | +fn get_derive_args<'a>(context: &'a RewriteContext, attr: &ast::Attribute) -> Option<Vec<&'a str>> { |
| 51 | + attr.meta_item_list().map(|meta_item_list| { |
| 52 | + meta_item_list |
| 53 | + .iter() |
| 54 | + .map(|nested_meta_item| context.snippet(nested_meta_item.span)) |
| 55 | + .collect() |
| 56 | + }) |
| 57 | +} |
| 58 | + |
| 59 | +// Format `#[derive(..)]`, using visual indent & mixed style when we need to go multiline. |
| 60 | +fn format_derive(context: &RewriteContext, derive_args: &[&str], shape: Shape) -> Option<String> { |
| 61 | + let mut result = String::with_capacity(128); |
| 62 | + result.push_str("#[derive("); |
| 63 | + // 11 = `#[derive()]` |
| 64 | + let initial_budget = shape.width.checked_sub(11)?; |
| 65 | + let mut budget = initial_budget; |
| 66 | + let num = derive_args.len(); |
| 67 | + for (i, a) in derive_args.iter().enumerate() { |
| 68 | + // 2 = `, ` or `)]` |
| 69 | + let width = a.len() + 2; |
| 70 | + if width > budget { |
| 71 | + if i > 0 { |
| 72 | + // Remove trailing whitespace. |
| 73 | + result.pop(); |
| 74 | + } |
| 75 | + result.push('\n'); |
| 76 | + // 9 = `#[derive(` |
| 77 | + result.push_str(&(shape.indent + 9).to_string(context.config)); |
| 78 | + budget = initial_budget; |
| 79 | + } else { |
| 80 | + budget = budget.checked_sub(width).unwrap_or(0); |
| 81 | + } |
| 82 | + result.push_str(a); |
| 83 | + if i != num - 1 { |
| 84 | + result.push_str(", ") |
| 85 | + } |
| 86 | + } |
| 87 | + result.push_str(")]"); |
| 88 | + Some(result) |
| 89 | +} |
| 90 | + |
| 91 | +/// Returns the first group of attributes that fills the given predicate. |
| 92 | +/// We consider two doc comments are in different group if they are separated by normal comments. |
| 93 | +fn take_while_with_pred<'a, P>( |
| 94 | + context: &RewriteContext, |
| 95 | + attrs: &'a [ast::Attribute], |
| 96 | + pred: P, |
| 97 | +) -> &'a [ast::Attribute] |
| 98 | +where |
| 99 | + P: Fn(&ast::Attribute) -> bool, |
| 100 | +{ |
| 101 | + let mut last_index = 0; |
| 102 | + let mut iter = attrs.iter().enumerate().peekable(); |
| 103 | + while let Some((i, attr)) = iter.next() { |
| 104 | + if !pred(attr) { |
| 105 | + break; |
| 106 | + } |
| 107 | + if let Some(&(_, next_attr)) = iter.peek() { |
| 108 | + // Extract comments between two attributes. |
| 109 | + let span_between_attr = mk_sp(attr.span.hi(), next_attr.span.lo()); |
| 110 | + let snippet = context.snippet(span_between_attr); |
| 111 | + if count_newlines(snippet) >= 2 || snippet.contains('/') { |
| 112 | + break; |
| 113 | + } |
| 114 | + } |
| 115 | + last_index = i; |
| 116 | + } |
| 117 | + if last_index == 0 { |
| 118 | + &[] |
| 119 | + } else { |
| 120 | + &attrs[..last_index + 1] |
| 121 | + } |
| 122 | +} |
| 123 | + |
| 124 | +/// Rewrite the same kind of attributes at the same time. This includes doc |
| 125 | +/// comments and derives. |
| 126 | +fn rewrite_first_group_attrs( |
| 127 | + context: &RewriteContext, |
| 128 | + attrs: &[ast::Attribute], |
| 129 | + shape: Shape, |
| 130 | +) -> Option<(usize, String)> { |
| 131 | + if attrs.is_empty() { |
| 132 | + return Some((0, String::new())); |
| 133 | + } |
| 134 | + // Rewrite doc comments |
| 135 | + let sugared_docs = take_while_with_pred(context, attrs, |a| a.is_sugared_doc); |
| 136 | + if !sugared_docs.is_empty() { |
| 137 | + let snippet = sugared_docs |
| 138 | + .iter() |
| 139 | + .map(|a| context.snippet(a.span)) |
| 140 | + .collect::<Vec<_>>() |
| 141 | + .join("\n"); |
| 142 | + return Some(( |
| 143 | + sugared_docs.len(), |
| 144 | + rewrite_doc_comment(&snippet, shape, context.config)?, |
| 145 | + )); |
| 146 | + } |
| 147 | + // Rewrite `#[derive(..)]`s. |
| 148 | + if context.config.merge_derives() { |
| 149 | + let derives = take_while_with_pred(context, attrs, is_derive); |
| 150 | + if !derives.is_empty() { |
| 151 | + let mut derive_args = vec![]; |
| 152 | + for derive in derives { |
| 153 | + derive_args.append(&mut get_derive_args(context, derive)?); |
| 154 | + } |
| 155 | + return Some((derives.len(), format_derive(context, &derive_args, shape)?)); |
| 156 | + } |
| 157 | + } |
| 158 | + // Rewrite the first attribute. |
| 159 | + Some((1, attrs[0].rewrite(context, shape)?)) |
| 160 | +} |
| 161 | + |
| 162 | +impl Rewrite for ast::NestedMetaItem { |
| 163 | + fn rewrite(&self, context: &RewriteContext, shape: Shape) -> Option<String> { |
| 164 | + match self.node { |
| 165 | + ast::NestedMetaItemKind::MetaItem(ref meta_item) => meta_item.rewrite(context, shape), |
| 166 | + ast::NestedMetaItemKind::Literal(ref l) => rewrite_literal(context, l, shape), |
| 167 | + } |
| 168 | + } |
| 169 | +} |
| 170 | + |
| 171 | +fn has_newlines_before_after_comment(comment: &str) -> (&str, &str) { |
| 172 | + // Look at before and after comment and see if there are any empty lines. |
| 173 | + let comment_begin = comment.chars().position(|c| c == '/'); |
| 174 | + let len = comment_begin.unwrap_or_else(|| comment.len()); |
| 175 | + let mlb = count_newlines(&comment[..len]) > 1; |
| 176 | + let mla = if comment_begin.is_none() { |
| 177 | + mlb |
| 178 | + } else { |
| 179 | + let comment_end = comment.chars().rev().position(|c| !c.is_whitespace()); |
| 180 | + let len = comment_end.unwrap(); |
| 181 | + comment |
| 182 | + .chars() |
| 183 | + .rev() |
| 184 | + .take(len) |
| 185 | + .filter(|c| *c == '\n') |
| 186 | + .count() > 1 |
| 187 | + }; |
| 188 | + (if mlb { "\n" } else { "" }, if mla { "\n" } else { "" }) |
| 189 | +} |
| 190 | + |
| 191 | +impl Rewrite for ast::MetaItem { |
| 192 | + fn rewrite(&self, context: &RewriteContext, shape: Shape) -> Option<String> { |
| 193 | + Some(match self.node { |
| 194 | + ast::MetaItemKind::Word => String::from(&*self.name.as_str()), |
| 195 | + ast::MetaItemKind::List(ref list) => { |
| 196 | + let name = self.name.as_str(); |
| 197 | + // 1 = `(`, 2 = `]` and `)` |
| 198 | + let item_shape = shape |
| 199 | + .visual_indent(0) |
| 200 | + .shrink_left(name.len() + 1) |
| 201 | + .and_then(|s| s.sub_width(2))?; |
| 202 | + let items = itemize_list( |
| 203 | + context.snippet_provider, |
| 204 | + list.iter(), |
| 205 | + ")", |
| 206 | + ",", |
| 207 | + |nested_meta_item| nested_meta_item.span.lo(), |
| 208 | + |nested_meta_item| nested_meta_item.span.hi(), |
| 209 | + |nested_meta_item| nested_meta_item.rewrite(context, item_shape), |
| 210 | + self.span.lo(), |
| 211 | + self.span.hi(), |
| 212 | + false, |
| 213 | + ); |
| 214 | + let item_vec = items.collect::<Vec<_>>(); |
| 215 | + let fmt = ListFormatting { |
| 216 | + tactic: DefinitiveListTactic::Mixed, |
| 217 | + separator: ",", |
| 218 | + trailing_separator: SeparatorTactic::Never, |
| 219 | + separator_place: SeparatorPlace::Back, |
| 220 | + shape: item_shape, |
| 221 | + ends_with_newline: false, |
| 222 | + preserve_newline: false, |
| 223 | + config: context.config, |
| 224 | + }; |
| 225 | + format!("{}({})", name, write_list(&item_vec, &fmt)?) |
| 226 | + } |
| 227 | + ast::MetaItemKind::NameValue(ref literal) => { |
| 228 | + let name = self.name.as_str(); |
| 229 | + // 3 = ` = ` |
| 230 | + let lit_shape = shape.shrink_left(name.len() + 3)?; |
| 231 | + // `rewrite_literal` returns `None` when `literal` exceeds max |
| 232 | + // width. Since a literal is basically unformattable unless it |
| 233 | + // is a string literal (and only if `format_strings` is set), |
| 234 | + // we might be better off ignoring the fact that the attribute |
| 235 | + // is longer than the max width and contiue on formatting. |
| 236 | + // See #2479 for example. |
| 237 | + let value = rewrite_literal(context, literal, lit_shape) |
| 238 | + .unwrap_or_else(|| context.snippet(literal.span).to_owned()); |
| 239 | + format!("{} = {}", name, value) |
| 240 | + } |
| 241 | + }) |
| 242 | + } |
| 243 | +} |
| 244 | + |
| 245 | +impl Rewrite for ast::Attribute { |
| 246 | + fn rewrite(&self, context: &RewriteContext, shape: Shape) -> Option<String> { |
| 247 | + let prefix = match self.style { |
| 248 | + ast::AttrStyle::Inner => "#!", |
| 249 | + ast::AttrStyle::Outer => "#", |
| 250 | + }; |
| 251 | + let snippet = context.snippet(self.span); |
| 252 | + if self.is_sugared_doc { |
| 253 | + let doc_shape = Shape { |
| 254 | + width: cmp::min(shape.width, context.config.comment_width()) |
| 255 | + .checked_sub(shape.indent.width()) |
| 256 | + .unwrap_or(0), |
| 257 | + ..shape |
| 258 | + }; |
| 259 | + rewrite_doc_comment(snippet, doc_shape, context.config) |
| 260 | + } else { |
| 261 | + if contains_comment(snippet) { |
| 262 | + return Some(snippet.to_owned()); |
| 263 | + } |
| 264 | + // 1 = `[` |
| 265 | + let shape = shape.offset_left(prefix.len() + 1)?; |
| 266 | + Some( |
| 267 | + self.meta() |
| 268 | + .and_then(|meta| meta.rewrite(context, shape)) |
| 269 | + .map_or_else(|| snippet.to_owned(), |rw| format!("{}[{}]", prefix, rw)), |
| 270 | + ) |
| 271 | + } |
| 272 | + } |
| 273 | +} |
| 274 | + |
| 275 | +impl<'a> Rewrite for [ast::Attribute] { |
| 276 | + fn rewrite(&self, context: &RewriteContext, shape: Shape) -> Option<String> { |
| 277 | + if self.is_empty() { |
| 278 | + return Some(String::new()); |
| 279 | + } |
| 280 | + let (first_group_len, first_group_str) = rewrite_first_group_attrs(context, self, shape)?; |
| 281 | + if self.len() == 1 || first_group_len == self.len() { |
| 282 | + Some(first_group_str) |
| 283 | + } else { |
| 284 | + let rest_str = self[first_group_len..].rewrite(context, shape)?; |
| 285 | + let missing_span = mk_sp( |
| 286 | + self[first_group_len - 1].span.hi(), |
| 287 | + self[first_group_len].span.lo(), |
| 288 | + ); |
| 289 | + // Preserve an empty line before/after doc comments. |
| 290 | + if self[0].is_sugared_doc || self[first_group_len].is_sugared_doc { |
| 291 | + let snippet = context.snippet(missing_span); |
| 292 | + let (mla, mlb) = has_newlines_before_after_comment(snippet); |
| 293 | + let comment = ::comment::recover_missing_comment_in_span( |
| 294 | + missing_span, |
| 295 | + shape.with_max_width(context.config), |
| 296 | + context, |
| 297 | + 0, |
| 298 | + )?; |
| 299 | + let comment = if comment.is_empty() { |
| 300 | + format!("\n{}", mlb) |
| 301 | + } else { |
| 302 | + format!("{}{}\n{}", mla, comment, mlb) |
| 303 | + }; |
| 304 | + Some(format!( |
| 305 | + "{}{}{}{}", |
| 306 | + first_group_str, |
| 307 | + comment, |
| 308 | + shape.indent.to_string(context.config), |
| 309 | + rest_str |
| 310 | + )) |
| 311 | + } else { |
| 312 | + combine_strs_with_missing_comments( |
| 313 | + context, |
| 314 | + &first_group_str, |
| 315 | + &rest_str, |
| 316 | + missing_span, |
| 317 | + shape, |
| 318 | + false, |
| 319 | + ) |
| 320 | + } |
| 321 | + } |
| 322 | + } |
| 323 | +} |
0 commit comments