rustfmt-nightly 0.2.4

Tool to find and fix Rust formatting issues
// Copyright 2017 The Rust Project Developers. See the COPYRIGHT
// file at the top-level directory of this distribution and at
// http://rust-lang.org/COPYRIGHT.
//
// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
// option. This file may not be copied, modified, or distributed
// except according to those terms.

// Format with vertical alignment.

use std::cmp;

use syntax::ast;
use syntax::codemap::{BytePos, Span};

use {Indent, Shape, Spanned};
use codemap::SpanUtils;
use comment::{combine_strs_with_missing_comments, contains_comment};
use expr::rewrite_field;
use items::{rewrite_struct_field, rewrite_struct_field_prefix};
use lists::{definitive_tactic, itemize_list, write_list, ListFormatting, ListTactic, Separator,
            SeparatorPlace};
use rewrite::{Rewrite, RewriteContext};
use utils::{contains_skip, is_attributes_extendable, mk_sp};

pub trait AlignedItem {
    fn skip(&self) -> bool;
    fn get_span(&self) -> Span;
    fn rewrite_prefix(&self, context: &RewriteContext, shape: Shape) -> Option<String>;
    fn rewrite_aligned_item(
        &self,
        context: &RewriteContext,
        shape: Shape,
        prefix_max_width: usize,
    ) -> Option<String>;
}

impl AlignedItem for ast::StructField {
    fn skip(&self) -> bool {
        contains_skip(&self.attrs)
    }

    fn get_span(&self) -> Span {
        self.span()
    }

    fn rewrite_prefix(&self, context: &RewriteContext, shape: Shape) -> Option<String> {
        let attrs_str = try_opt!(self.attrs.rewrite(context, shape));
        let missing_span = if self.attrs.is_empty() {
            mk_sp(self.span.lo, self.span.lo)
        } else {
            mk_sp(self.attrs.last().unwrap().span.hi, self.span.lo)
        };
        rewrite_struct_field_prefix(context, self).and_then(|field_str| {
            combine_strs_with_missing_comments(
                context,
                &attrs_str,
                &field_str,
                missing_span,
                shape,
                is_attributes_extendable(&attrs_str),
            )
        })
    }

    fn rewrite_aligned_item(
        &self,
        context: &RewriteContext,
        shape: Shape,
        prefix_max_width: usize,
    ) -> Option<String> {
        rewrite_struct_field(context, self, shape, prefix_max_width)
    }
}

impl AlignedItem for ast::Field {
    fn skip(&self) -> bool {
        contains_skip(&self.attrs)
    }

    fn get_span(&self) -> Span {
        self.span()
    }

    fn rewrite_prefix(&self, context: &RewriteContext, shape: Shape) -> Option<String> {
        let attrs_str = try_opt!(self.attrs.rewrite(context, shape));
        let name = &self.ident.node.to_string();
        let missing_span = if self.attrs.is_empty() {
            mk_sp(self.span.lo, self.span.lo)
        } else {
            mk_sp(self.attrs.last().unwrap().span.hi, self.span.lo)
        };
        combine_strs_with_missing_comments(
            context,
            &attrs_str,
            name,
            missing_span,
            shape,
            is_attributes_extendable(&attrs_str),
        )
    }

    fn rewrite_aligned_item(
        &self,
        context: &RewriteContext,
        shape: Shape,
        prefix_max_width: usize,
    ) -> Option<String> {
        rewrite_field(context, self, shape, prefix_max_width)
    }
}

pub fn rewrite_with_alignment<T: AlignedItem>(
    fields: &[T],
    context: &RewriteContext,
    shape: Shape,
    span: Span,
    one_line_width: usize,
) -> Option<String> {
    let (spaces, group_index) = if context.config.struct_field_align_threshold() > 0 {
        group_aligned_items(context, fields)
    } else {
        ("", fields.len() - 1)
    };
    let init = &fields[0..group_index + 1];
    let rest = &fields[group_index + 1..];
    let init_last_pos = if rest.is_empty() {
        span.hi
    } else {
        // Decide whether the missing comments should stick to init or rest.
        let init_hi = init[init.len() - 1].get_span().hi;
        let rest_lo = rest[0].get_span().lo;
        let missing_span = mk_sp(init_hi, rest_lo);
        let missing_span = mk_sp(
            context.codemap.span_after(missing_span, ","),
            missing_span.hi,
        );

        let snippet = context.snippet(missing_span);
        if snippet.trim_left().starts_with("//") {
            let offset = snippet.lines().next().map_or(0, |l| l.len());
            // 2 = "," + "\n"
            init_hi + BytePos(offset as u32 + 2)
        } else if snippet.trim_left().starts_with("/*") {
            let comment_lines = snippet
                .lines()
                .position(|line| line.trim_right().ends_with("*/"))
                .unwrap_or(0);

            let offset = snippet
                .lines()
                .take(comment_lines + 1)
                .collect::<Vec<_>>()
                .join("\n")
                .len();

            init_hi + BytePos(offset as u32 + 2)
        } else {
            missing_span.lo
        }
    };
    let init_span = mk_sp(span.lo, init_last_pos);
    let one_line_width = if rest.is_empty() { one_line_width } else { 0 };
    let result = try_opt!(rewrite_aligned_items_inner(
        context,
        init,
        init_span,
        shape.indent,
        one_line_width,
    ));
    if rest.is_empty() {
        Some(result + spaces)
    } else {
        let rest_span = mk_sp(init_last_pos, span.hi);
        let rest_str = try_opt!(rewrite_with_alignment(
            rest,
            context,
            shape,
            rest_span,
            one_line_width,
        ));
        Some(
            result + spaces + "\n" +
                &shape
                    .indent
                    .block_indent(context.config)
                    .to_string(context.config) + &rest_str,
        )
    }
}

fn struct_field_preix_max_min_width<T: AlignedItem>(
    context: &RewriteContext,
    fields: &[T],
    shape: Shape,
) -> (usize, usize) {
    fields
        .iter()
        .map(|field| {
            field
                .rewrite_prefix(context, shape)
                .and_then(|field_str| if field_str.contains('\n') {
                    None
                } else {
                    Some(field_str.len())
                })
        })
        .fold(Some((0, ::std::usize::MAX)), |acc, len| match (acc, len) {
            (Some((max_len, min_len)), Some(len)) => {
                Some((cmp::max(max_len, len), cmp::min(min_len, len)))
            }
            _ => None,
        })
        .unwrap_or((0, 0))
}

fn rewrite_aligned_items_inner<T: AlignedItem>(
    context: &RewriteContext,
    fields: &[T],
    span: Span,
    offset: Indent,
    one_line_width: usize,
) -> Option<String> {
    let item_indent = offset.block_indent(context.config);
    // 1 = ","
    let item_shape = try_opt!(Shape::indented(item_indent, context.config).sub_width(1));
    let (mut field_prefix_max_width, field_prefix_min_width) =
        struct_field_preix_max_min_width(context, fields, item_shape);
    let max_diff = field_prefix_max_width
        .checked_sub(field_prefix_min_width)
        .unwrap_or(0);
    if max_diff > context.config.struct_field_align_threshold() {
        field_prefix_max_width = 0;
    }

    let items = itemize_list(
        context.codemap,
        fields.iter(),
        "}",
        |field| field.get_span().lo,
        |field| field.get_span().hi,
        |field| field.rewrite_aligned_item(context, item_shape, field_prefix_max_width),
        span.lo,
        span.hi,
        false,
    ).collect::<Vec<_>>();

    let tactic = definitive_tactic(
        &items,
        ListTactic::HorizontalVertical,
        Separator::Comma,
        one_line_width,
    );

    let fmt = ListFormatting {
        tactic: tactic,
        separator: ",",
        trailing_separator: context.config.trailing_comma(),
        separator_place: SeparatorPlace::Back,
        shape: item_shape,
        ends_with_newline: true,
        preserve_newline: true,
        config: context.config,
    };
    write_list(&items, &fmt)
}

fn group_aligned_items<T: AlignedItem>(
    context: &RewriteContext,
    fields: &[T],
) -> (&'static str, usize) {
    let mut index = 0;
    for i in 0..fields.len() - 1 {
        if fields[i].skip() {
            return ("", index);
        }
        // See if there are comments or empty lines between fields.
        let span = mk_sp(fields[i].get_span().hi, fields[i + 1].get_span().lo);
        let snippet = context
            .snippet(span)
            .lines()
            .skip(1)
            .collect::<Vec<_>>()
            .join("\n");
        let spacings = if snippet
            .lines()
            .rev()
            .skip(1)
            .find(|l| l.trim().is_empty())
            .is_some()
        {
            "\n"
        } else {
            ""
        };
        if contains_comment(&snippet) || snippet.lines().count() > 1 {
            return (spacings, index);
        }
        index += 1;
    }
    ("", index)
}