use std::borrow::Cow;
use rustc_ast::ast::{
self, Attribute, MetaItem, MetaItemInner, MetaItemKind, NodeId, Path, Visibility,
VisibilityKind,
};
use rustc_ast::ptr;
use rustc_ast_pretty::pprust;
use rustc_span::{BytePos, LocalExpnId, Span, Symbol, SyntaxContext, sym, symbol};
use unicode_width::UnicodeWidthStr;
use crate::comment::{CharClasses, FullCodeCharKind, LineClasses, filter_normal_code};
use crate::config::{Config, StyleEdition};
use crate::rewrite::RewriteContext;
use crate::shape::{Indent, Shape};
#[inline]
pub(crate) fn depr_skip_annotation() -> Symbol {
Symbol::intern("rustfmt_skip")
}
#[inline]
pub(crate) fn skip_annotation() -> Symbol {
Symbol::intern("rustfmt::skip")
}
pub(crate) fn rewrite_ident<'a>(context: &'a RewriteContext<'_>, ident: symbol::Ident) -> &'a str {
context.snippet(ident.span)
}
pub(crate) fn extra_offset(text: &str, shape: Shape) -> usize {
match text.rfind('\n') {
Some(idx) => text.len().saturating_sub(idx + 1 + shape.used_width()),
None => text.len(),
}
}
pub(crate) fn is_same_visibility(a: &Visibility, b: &Visibility) -> bool {
match (&a.kind, &b.kind) {
(
VisibilityKind::Restricted { path: p, .. },
VisibilityKind::Restricted { path: q, .. },
) => pprust::path_to_string(p) == pprust::path_to_string(q),
(VisibilityKind::Public, VisibilityKind::Public)
| (VisibilityKind::Inherited, VisibilityKind::Inherited) => true,
_ => false,
}
}
pub(crate) fn format_visibility(
context: &RewriteContext<'_>,
vis: &Visibility,
) -> Cow<'static, str> {
match vis.kind {
VisibilityKind::Public => Cow::from("pub "),
VisibilityKind::Inherited => Cow::from(""),
VisibilityKind::Restricted { ref path, .. } => {
let Path { ref segments, .. } = **path;
let mut segments_iter = segments.iter().map(|seg| rewrite_ident(context, seg.ident));
if path.is_global() {
segments_iter
.next()
.expect("Non-global path in pub(restricted)?");
}
let is_keyword = |s: &str| s == "crate" || s == "self" || s == "super";
let path = segments_iter.collect::<Vec<_>>().join("::");
let in_str = if is_keyword(&path) { "" } else { "in " };
Cow::from(format!("pub({in_str}{path}) "))
}
}
}
#[inline]
pub(crate) fn format_coro(coroutine_kind: &ast::CoroutineKind) -> &'static str {
match coroutine_kind {
ast::CoroutineKind::Async { .. } => "async ",
ast::CoroutineKind::Gen { .. } => "gen ",
ast::CoroutineKind::AsyncGen { .. } => "async gen ",
}
}
#[inline]
pub(crate) fn format_constness(constness: ast::Const) -> &'static str {
match constness {
ast::Const::Yes(..) => "const ",
ast::Const::No => "",
}
}
#[inline]
pub(crate) fn format_constness_right(constness: ast::Const) -> &'static str {
match constness {
ast::Const::Yes(..) => " const",
ast::Const::No => "",
}
}
#[inline]
pub(crate) fn format_defaultness(defaultness: ast::Defaultness) -> &'static str {
match defaultness {
ast::Defaultness::Default(..) => "default ",
ast::Defaultness::Final => "",
}
}
#[inline]
pub(crate) fn format_safety(unsafety: ast::Safety) -> &'static str {
match unsafety {
ast::Safety::Unsafe(..) => "unsafe ",
ast::Safety::Safe(..) => "safe ",
ast::Safety::Default => "",
}
}
#[inline]
pub(crate) fn format_auto(is_auto: ast::IsAuto) -> &'static str {
match is_auto {
ast::IsAuto::Yes => "auto ",
ast::IsAuto::No => "",
}
}
#[inline]
pub(crate) fn format_mutability(mutability: ast::Mutability) -> &'static str {
match mutability {
ast::Mutability::Mut => "mut ",
ast::Mutability::Not => "",
}
}
#[inline]
pub(crate) fn format_extern(ext: ast::Extern, explicit_abi: bool) -> Cow<'static, str> {
match ext {
ast::Extern::None => Cow::from(""),
ast::Extern::Implicit(_) if explicit_abi => Cow::from("extern \"C\" "),
ast::Extern::Implicit(_) => Cow::from("extern "),
ast::Extern::Explicit(abi, _) if abi.symbol_unescaped == sym::C && !explicit_abi => {
Cow::from("extern ")
}
ast::Extern::Explicit(abi, _) => {
Cow::from(format!(r#"extern "{}" "#, abi.symbol_unescaped))
}
}
}
#[inline]
pub(crate) fn ptr_vec_to_ref_vec<T>(vec: &[ptr::P<T>]) -> Vec<&T> {
vec.iter().map(|x| &**x).collect::<Vec<_>>()
}
#[inline]
pub(crate) fn filter_attributes(
attrs: &[ast::Attribute],
style: ast::AttrStyle,
) -> Vec<ast::Attribute> {
attrs
.iter()
.filter(|a| a.style == style)
.cloned()
.collect::<Vec<_>>()
}
#[inline]
pub(crate) fn inner_attributes(attrs: &[ast::Attribute]) -> Vec<ast::Attribute> {
filter_attributes(attrs, ast::AttrStyle::Inner)
}
#[inline]
pub(crate) fn outer_attributes(attrs: &[ast::Attribute]) -> Vec<ast::Attribute> {
filter_attributes(attrs, ast::AttrStyle::Outer)
}
#[inline]
pub(crate) fn is_single_line(s: &str) -> bool {
!s.chars().any(|c| c == '\n')
}
#[inline]
pub(crate) fn first_line_contains_single_line_comment(s: &str) -> bool {
s.lines().next().map_or(false, |l| l.contains("//"))
}
#[inline]
pub(crate) fn last_line_contains_single_line_comment(s: &str) -> bool {
s.lines().last().map_or(false, |l| l.contains("//"))
}
#[inline]
pub(crate) fn is_attributes_extendable(attrs_str: &str) -> bool {
!attrs_str.contains('\n') && !last_line_contains_single_line_comment(attrs_str)
}
#[inline]
pub(crate) fn first_line_width(s: &str) -> usize {
unicode_str_width(s.splitn(2, '\n').next().unwrap_or(""))
}
#[inline]
pub(crate) fn last_line_width(s: &str) -> usize {
unicode_str_width(s.rsplitn(2, '\n').next().unwrap_or(""))
}
#[inline]
pub(crate) fn last_line_used_width(s: &str, offset: usize) -> usize {
if s.contains('\n') {
last_line_width(s)
} else {
offset + unicode_str_width(s)
}
}
#[inline]
pub(crate) fn trimmed_last_line_width(s: &str) -> usize {
unicode_str_width(match s.rfind('\n') {
Some(n) => s[(n + 1)..].trim(),
None => s.trim(),
})
}
#[inline]
pub(crate) fn last_line_extendable(s: &str) -> bool {
if s.ends_with("\"#") {
return true;
}
for c in s.chars().rev() {
match c {
'(' | ')' | ']' | '}' | '?' | '>' => continue,
'\n' => break,
_ if c.is_whitespace() => continue,
_ => return false,
}
}
true
}
#[inline]
fn is_skip(meta_item: &MetaItem) -> bool {
match meta_item.kind {
MetaItemKind::Word => {
let path_str = pprust::path_to_string(&meta_item.path);
path_str == skip_annotation().as_str() || path_str == depr_skip_annotation().as_str()
}
MetaItemKind::List(ref l) => {
meta_item.has_name(sym::cfg_attr) && l.len() == 2 && is_skip_nested(&l[1])
}
_ => false,
}
}
#[inline]
fn is_skip_nested(meta_item: &MetaItemInner) -> bool {
match meta_item {
MetaItemInner::MetaItem(ref mi) => is_skip(mi),
MetaItemInner::Lit(_) => false,
}
}
#[inline]
pub(crate) fn contains_skip(attrs: &[Attribute]) -> bool {
attrs
.iter()
.any(|a| a.meta().map_or(false, |a| is_skip(&a)))
}
#[inline]
pub(crate) fn semicolon_for_expr(context: &RewriteContext<'_>, expr: &ast::Expr) -> bool {
if context.is_macro_def {
return false;
}
match expr.kind {
ast::ExprKind::Ret(..) | ast::ExprKind::Continue(..) | ast::ExprKind::Break(..) => {
context.config.trailing_semicolon()
}
_ => false,
}
}
#[inline]
pub(crate) fn semicolon_for_stmt(
context: &RewriteContext<'_>,
stmt: &ast::Stmt,
is_last_expr: bool,
) -> bool {
match stmt.kind {
ast::StmtKind::Semi(ref expr) => match expr.kind {
ast::ExprKind::While(..) | ast::ExprKind::Loop(..) | ast::ExprKind::ForLoop { .. } => {
false
}
ast::ExprKind::Break(..) | ast::ExprKind::Continue(..) | ast::ExprKind::Ret(..) => {
context.config.trailing_semicolon() || !is_last_expr
}
_ => true,
},
ast::StmtKind::Expr(..) => false,
_ => true,
}
}
#[inline]
pub(crate) fn stmt_expr(stmt: &ast::Stmt) -> Option<&ast::Expr> {
match stmt.kind {
ast::StmtKind::Expr(ref expr) => Some(expr),
_ => None,
}
}
pub(crate) fn count_lf_crlf(input: &str) -> (usize, usize) {
let mut lf = 0;
let mut crlf = 0;
let mut is_crlf = false;
for c in input.as_bytes() {
match c {
b'\r' => is_crlf = true,
b'\n' if is_crlf => crlf += 1,
b'\n' => lf += 1,
_ => is_crlf = false,
}
}
(lf, crlf)
}
pub(crate) fn count_newlines(input: &str) -> usize {
bytecount::count(input.as_bytes(), b'\n')
}
macro_rules! source {
($this:ident, $sp:expr) => {
$sp.source_callsite()
};
}
pub(crate) fn mk_sp(lo: BytePos, hi: BytePos) -> Span {
Span::new(lo, hi, SyntaxContext::root(), None)
}
pub(crate) fn mk_sp_lo_plus_one(lo: BytePos) -> Span {
Span::new(lo, lo + BytePos(1), SyntaxContext::root(), None)
}
macro_rules! out_of_file_lines_range {
($self:ident, $span:expr) => {
!$self.config.file_lines().is_all()
&& !$self
.config
.file_lines()
.intersects(&$self.psess.lookup_line_range($span))
};
}
macro_rules! skip_out_of_file_lines_range_err {
($self:ident, $span:expr) => {
if out_of_file_lines_range!($self, $span) {
return Err(RewriteError::SkipFormatting);
}
};
}
macro_rules! skip_out_of_file_lines_range_visitor {
($self:ident, $span:expr) => {
if out_of_file_lines_range!($self, $span) {
$self.push_rewrite($span, None);
return;
}
};
}
pub(crate) fn wrap_str(s: String, max_width: usize, shape: Shape) -> Option<String> {
if filtered_str_fits(&s, max_width, shape) {
Some(s)
} else {
None
}
}
pub(crate) fn filtered_str_fits(snippet: &str, max_width: usize, shape: Shape) -> bool {
let snippet = &filter_normal_code(snippet);
if !snippet.is_empty() {
if first_line_width(snippet) > shape.width {
return false;
}
if is_single_line(snippet) {
return true;
}
if snippet
.lines()
.skip(1)
.any(|line| unicode_str_width(line) > max_width)
{
return false;
}
if last_line_width(snippet) > shape.used_width() + shape.width {
return false;
}
}
true
}
#[inline]
pub(crate) fn colon_spaces(config: &Config) -> &'static str {
let before = config.space_before_colon();
let after = config.space_after_colon();
match (before, after) {
(true, true) => " : ",
(true, false) => " :",
(false, true) => ": ",
(false, false) => ":",
}
}
#[inline]
pub(crate) fn left_most_sub_expr(e: &ast::Expr) -> &ast::Expr {
match e.kind {
ast::ExprKind::Call(ref e, _)
| ast::ExprKind::Binary(_, ref e, _)
| ast::ExprKind::Cast(ref e, _)
| ast::ExprKind::Type(ref e, _)
| ast::ExprKind::Assign(ref e, _, _)
| ast::ExprKind::AssignOp(_, ref e, _)
| ast::ExprKind::Field(ref e, _)
| ast::ExprKind::Index(ref e, _, _)
| ast::ExprKind::Range(Some(ref e), _, _)
| ast::ExprKind::Try(ref e) => left_most_sub_expr(e),
_ => e,
}
}
#[inline]
pub(crate) fn starts_with_newline(s: &str) -> bool {
s.starts_with('\n') || s.starts_with("\r\n")
}
#[inline]
pub(crate) fn first_line_ends_with(s: &str, c: char) -> bool {
s.lines().next().map_or(false, |l| l.ends_with(c))
}
pub(crate) fn is_block_expr(context: &RewriteContext<'_>, expr: &ast::Expr, repr: &str) -> bool {
match expr.kind {
ast::ExprKind::MacCall(..)
| ast::ExprKind::FormatArgs(..)
| ast::ExprKind::Call(..)
| ast::ExprKind::MethodCall(..)
| ast::ExprKind::Array(..)
| ast::ExprKind::Struct(..)
| ast::ExprKind::While(..)
| ast::ExprKind::If(..)
| ast::ExprKind::Block(..)
| ast::ExprKind::ConstBlock(..)
| ast::ExprKind::Gen(..)
| ast::ExprKind::Loop(..)
| ast::ExprKind::ForLoop { .. }
| ast::ExprKind::TryBlock(..)
| ast::ExprKind::Match(..) => repr.contains('\n'),
ast::ExprKind::Paren(ref expr)
| ast::ExprKind::Binary(_, _, ref expr)
| ast::ExprKind::Index(_, ref expr, _)
| ast::ExprKind::Unary(_, ref expr)
| ast::ExprKind::Try(ref expr)
| ast::ExprKind::Yield(Some(ref expr)) => is_block_expr(context, expr, repr),
ast::ExprKind::Closure(ref closure) => is_block_expr(context, &closure.body, repr),
ast::ExprKind::Lit(_) => {
repr.contains('\n') && trimmed_last_line_width(repr) <= context.config.tab_spaces()
}
ast::ExprKind::AddrOf(..)
| ast::ExprKind::Assign(..)
| ast::ExprKind::AssignOp(..)
| ast::ExprKind::Await(..)
| ast::ExprKind::Break(..)
| ast::ExprKind::Cast(..)
| ast::ExprKind::Continue(..)
| ast::ExprKind::Dummy
| ast::ExprKind::Err(_)
| ast::ExprKind::Field(..)
| ast::ExprKind::IncludedBytes(..)
| ast::ExprKind::InlineAsm(..)
| ast::ExprKind::OffsetOf(..)
| ast::ExprKind::Let(..)
| ast::ExprKind::Path(..)
| ast::ExprKind::Range(..)
| ast::ExprKind::Repeat(..)
| ast::ExprKind::Ret(..)
| ast::ExprKind::Become(..)
| ast::ExprKind::Yeet(..)
| ast::ExprKind::Tup(..)
| ast::ExprKind::Type(..)
| ast::ExprKind::Yield(None)
| ast::ExprKind::Underscore => false,
}
}
pub(crate) fn remove_trailing_white_spaces(text: &str) -> String {
let mut buffer = String::with_capacity(text.len());
let mut space_buffer = String::with_capacity(128);
for (char_kind, c) in CharClasses::new(text.chars()) {
match c {
'\n' => {
if char_kind == FullCodeCharKind::InString {
buffer.push_str(&space_buffer);
}
space_buffer.clear();
buffer.push('\n');
}
_ if c.is_whitespace() => {
space_buffer.push(c);
}
_ => {
if !space_buffer.is_empty() {
buffer.push_str(&space_buffer);
space_buffer.clear();
}
buffer.push(c);
}
}
}
buffer
}
pub(crate) fn trim_left_preserve_layout(
orig: &str,
indent: Indent,
config: &Config,
) -> Option<String> {
let mut lines = LineClasses::new(orig);
let first_line = lines.next().map(|(_, s)| s.trim_end().to_owned())?;
let mut trimmed_lines = Vec::with_capacity(16);
let mut veto_trim = false;
let min_prefix_space_width = lines
.filter_map(|(kind, line)| {
let mut trimmed = true;
let prefix_space_width = if is_empty_line(&line) {
None
} else {
Some(get_prefix_space_width(config, &line))
};
let new_veto_trim_value = (kind == FullCodeCharKind::InString
|| (config.style_edition() >= StyleEdition::Edition2024
&& kind == FullCodeCharKind::InStringCommented))
&& !line.ends_with('\\');
let line = if veto_trim || new_veto_trim_value {
veto_trim = new_veto_trim_value;
trimmed = false;
line
} else {
line.trim().to_owned()
};
trimmed_lines.push((trimmed, line, prefix_space_width));
match kind {
FullCodeCharKind::InStringCommented | FullCodeCharKind::EndStringCommented
if config.style_edition() >= StyleEdition::Edition2024 =>
{
None
}
FullCodeCharKind::InString | FullCodeCharKind::EndString => None,
_ => prefix_space_width,
}
})
.min()?;
Some(
first_line
+ "\n"
+ &trimmed_lines
.iter()
.map(
|&(trimmed, ref line, prefix_space_width)| match prefix_space_width {
_ if !trimmed => line.to_owned(),
Some(original_indent_width) => {
let new_indent_width = indent.width()
+ original_indent_width.saturating_sub(min_prefix_space_width);
let new_indent = Indent::from_width(config, new_indent_width);
format!("{}{}", new_indent.to_string(config), line)
}
None => String::new(),
},
)
.collect::<Vec<_>>()
.join("\n"),
)
}
pub(crate) fn indent_next_line(kind: FullCodeCharKind, line: &str, config: &Config) -> bool {
if kind.is_string() {
config.format_strings() && line.ends_with('\\')
} else if config.style_edition() >= StyleEdition::Edition2024 {
!kind.is_commented_string()
} else {
true
}
}
pub(crate) fn is_empty_line(s: &str) -> bool {
s.is_empty() || s.chars().all(char::is_whitespace)
}
fn get_prefix_space_width(config: &Config, s: &str) -> usize {
let mut width = 0;
for c in s.chars() {
match c {
' ' => width += 1,
'\t' => width += config.tab_spaces(),
_ => return width,
}
}
width
}
pub(crate) trait NodeIdExt {
fn root() -> Self;
}
impl NodeIdExt for NodeId {
fn root() -> NodeId {
NodeId::placeholder_from_expn_id(LocalExpnId::ROOT)
}
}
pub(crate) fn unicode_str_width(s: &str) -> usize {
s.width()
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn test_remove_trailing_white_spaces() {
let s = " r#\"\n test\n \"#";
assert_eq!(remove_trailing_white_spaces(s), s);
}
#[test]
fn test_trim_left_preserve_layout() {
let s = "aaa\n\tbbb\n ccc";
let config = Config::default();
let indent = Indent::new(4, 0);
assert_eq!(
trim_left_preserve_layout(s, indent, &config),
Some("aaa\n bbb\n ccc".to_string())
);
}
}