wat_formatter 0.10.1

WebAssembly Text Format formatter.
Documentation
use self::{instr::*, module::*, ty::*};
use crate::config::{FormatOptions, LanguageOptions, MultiLine, WrapBefore};
use std::iter;
use tiny_pretty::Doc;
use wat_syntax::{AmberNode, AmberToken, NodeOrToken, SyntaxKind, ast::*};

mod instr;
mod module;
mod ty;

pub(super) struct Ctx<'a> {
    pub indent_width: usize,
    pub options: &'a LanguageOptions,
}
impl<'a> Ctx<'a> {
    pub(crate) fn new(options: &'a FormatOptions) -> Self {
        Self {
            indent_width: options.layout.indent_width,
            options: &options.language,
        }
    }

    pub(crate) fn format_right_paren(&self, node: AmberNode) -> Doc<'static> {
        let mut nodes_or_tokens = node.children_with_tokens().rev();
        let docs = if nodes_or_tokens
            .find_map(|node_or_token| match node_or_token {
                NodeOrToken::Token(token) if token.kind() == SyntaxKind::R_PAREN => Some(token),
                _ => None,
            })
            .and_then(|_| {
                nodes_or_tokens
                    .map_while(NodeOrToken::into_token)
                    .find(|token| token.kind() != SyntaxKind::WHITESPACE)
            })
            .is_some_and(|token| token.kind() == SyntaxKind::LINE_COMMENT)
        {
            vec![Doc::hard_line(), Doc::text(")")]
        } else if self.options.split_closing_parens {
            vec![Doc::line_or_nil(), Doc::text(")")]
        } else {
            vec![Doc::text(")")]
        };
        Doc::list(docs)
    }
}

pub(crate) fn format_node<'a>(node: AmberNode<'a>, ctx: &Ctx) -> Option<Doc<'a>> {
    match node.kind() {
        SyntaxKind::MODULE_NAME => Some(format_module_name(node)),
        SyntaxKind::NAME => Some(format_name(node)),
        SyntaxKind::NUM_TYPE => Some(format_num_type(node)),
        SyntaxKind::VEC_TYPE => Some(format_vec_type(node)),
        SyntaxKind::REF_TYPE => Some(format_ref_type(node, ctx)),
        SyntaxKind::HEAP_TYPE => Some(format_heap_type(node)),
        SyntaxKind::PACKED_TYPE => Some(format_packed_type(node)),
        SyntaxKind::FIELD_TYPE => Some(format_field_type(node, ctx)),
        SyntaxKind::STRUCT_TYPE => Some(format_struct_type(node, ctx)),
        SyntaxKind::ARRAY_TYPE => Some(format_array_type(node, ctx)),
        SyntaxKind::FUNC_TYPE => Some(format_func_type(node, ctx)),
        SyntaxKind::CONT_TYPE => Some(format_cont_type(node, ctx)),
        SyntaxKind::PARAM => Some(format_param(node, ctx)),
        SyntaxKind::RESULT => Some(format_result(node, ctx)),
        SyntaxKind::FIELD => Some(format_field(node, ctx)),
        SyntaxKind::SUB_TYPE => Some(format_sub_type(node, ctx)),
        SyntaxKind::TABLE_TYPE => Some(format_table_type(node, ctx)),
        SyntaxKind::MEM_TYPE => Some(format_mem_type(node, ctx)),
        SyntaxKind::ADDR_TYPE => Some(format_addr_type(node)),
        SyntaxKind::GLOBAL_TYPE => Some(format_global_type(node, ctx)),
        SyntaxKind::PLAIN_INSTR => Some(format_plain_instr(node, ctx)),
        SyntaxKind::BLOCK_BLOCK => Some(format_block_block(node, ctx)),
        SyntaxKind::BLOCK_LOOP => Some(format_block_loop(node, ctx)),
        SyntaxKind::BLOCK_IF => Some(format_block_if(node, ctx)),
        SyntaxKind::BLOCK_IF_THEN => Some(format_block_if_then(node, ctx)),
        SyntaxKind::BLOCK_IF_ELSE => Some(format_block_if_else(node, ctx)),
        SyntaxKind::BLOCK_TRY_TABLE => Some(format_block_try_table(node, ctx)),
        SyntaxKind::CATCH => Some(format_catch(node, ctx)),
        SyntaxKind::CATCH_ALL => Some(format_catch_all(node, ctx)),
        SyntaxKind::MEM_ARG => Some(format_mem_arg(node)),
        SyntaxKind::ON_CLAUSE => Some(format_on_clause(node, ctx)),
        SyntaxKind::IMMEDIATE => Some(format_immediate(node, ctx)),
        SyntaxKind::TYPE_USE => Some(format_type_use(node, ctx)),
        SyntaxKind::LIMITS => Some(format_limits(node, ctx)),
        SyntaxKind::IMPORT => Some(format_import(node, ctx)),
        SyntaxKind::EXPORT => Some(format_export(node, ctx)),
        SyntaxKind::EXTERN_TYPE_FUNC => Some(format_extern_type_func(node, ctx)),
        SyntaxKind::EXTERN_TYPE_TABLE => Some(format_extern_type_table(node, ctx)),
        SyntaxKind::EXTERN_TYPE_MEMORY => Some(format_extern_type_memory(node, ctx)),
        SyntaxKind::EXTERN_TYPE_GLOBAL => Some(format_extern_type_global(node, ctx)),
        SyntaxKind::EXTERN_TYPE_TAG => Some(format_extern_type_tag(node, ctx)),
        SyntaxKind::EXTERN_IDX_FUNC => Some(format_extern_idx(node, ctx)),
        SyntaxKind::EXTERN_IDX_TABLE => Some(format_extern_idx(node, ctx)),
        SyntaxKind::EXTERN_IDX_MEMORY => Some(format_extern_idx(node, ctx)),
        SyntaxKind::EXTERN_IDX_GLOBAL => Some(format_extern_idx(node, ctx)),
        SyntaxKind::EXTERN_IDX_TAG => Some(format_extern_idx(node, ctx)),
        SyntaxKind::INDEX => Some(format_index(node)),
        SyntaxKind::LOCAL => Some(format_local(node, ctx)),
        SyntaxKind::MEM_PAGE_SIZE => Some(format_mem_page_size(node, ctx)),
        SyntaxKind::MEM_USE => Some(format_mem_use(node, ctx)),
        SyntaxKind::OFFSET => Some(format_offset(node, ctx)),
        SyntaxKind::ELEM => Some(format_elem(node, ctx)),
        SyntaxKind::ELEM_LIST => Some(format_elem_list(node, ctx)),
        SyntaxKind::ELEM_EXPR => Some(format_elem_expr(node, ctx)),
        SyntaxKind::TABLE_USE => Some(format_table_use(node, ctx)),
        SyntaxKind::DATA => Some(format_data(node, ctx)),
        SyntaxKind::MODULE => Some(format_module(node, ctx)),
        SyntaxKind::MODULE_FIELD_DATA => Some(format_module_field_data(node, ctx)),
        SyntaxKind::MODULE_FIELD_ELEM => Some(format_module_field_elem(node, ctx)),
        SyntaxKind::MODULE_FIELD_EXPORT => Some(format_module_field_export(node, ctx)),
        SyntaxKind::MODULE_FIELD_FUNC => Some(format_module_field_func(node, ctx)),
        SyntaxKind::MODULE_FIELD_GLOBAL => Some(format_module_field_global(node, ctx)),
        SyntaxKind::MODULE_FIELD_IMPORT => Some(format_module_field_import(node, ctx)),
        SyntaxKind::MODULE_FIELD_MEMORY => Some(format_module_field_memory(node, ctx)),
        SyntaxKind::MODULE_FIELD_START => Some(format_module_field_start(node, ctx)),
        SyntaxKind::MODULE_FIELD_TABLE => Some(format_module_field_table(node, ctx)),
        SyntaxKind::MODULE_FIELD_TAG => Some(format_module_field_tag(node, ctx)),
        SyntaxKind::TYPE_DEF => Some(format_type_def(node, ctx)),
        SyntaxKind::REC_TYPE => Some(format_rec_type(node, ctx)),
        SyntaxKind::ROOT => Some(format_root(node, ctx)),
        _ => None,
    }
}

pub(crate) fn format_root<'a>(root: AmberNode<'a>, ctx: &Ctx) -> Doc<'a> {
    let mut docs = Vec::with_capacity(2);

    let mut nodes_or_tokens = root.children_with_tokens().enumerate().peekable();
    let mut prev_kind = SyntaxKind::WHITESPACE;
    while let Some((index, node_or_token)) = nodes_or_tokens.next() {
        let kind = node_or_token.kind();
        match node_or_token {
            NodeOrToken::Node(module) => {
                if should_ignore(module, root, ctx) {
                    reflow(module.green().to_string(), &mut docs);
                } else {
                    docs.push(format_module(module, ctx));
                }
            }
            NodeOrToken::Token(token) => match kind {
                SyntaxKind::LINE_COMMENT => {
                    docs.push(format_line_comment(token.text(), ctx));
                }
                SyntaxKind::BLOCK_COMMENT => {
                    docs.push(format_block_comment(token.text(), ctx));
                }
                SyntaxKind::WHITESPACE => {
                    if index > 0 && nodes_or_tokens.peek().is_some() {
                        match token.text().chars().filter(|c| *c == '\n').count() {
                            0 => {
                                if prev_kind == SyntaxKind::LINE_COMMENT {
                                    docs.push(Doc::hard_line());
                                } else {
                                    docs.push(Doc::space());
                                }
                            }
                            1 => {
                                docs.push(Doc::hard_line());
                            }
                            _ => {
                                docs.push(Doc::empty_line());
                                docs.push(Doc::hard_line());
                            }
                        }
                    }
                }
                _ => docs.push(Doc::text(token.text())),
            },
        }
        prev_kind = kind;
    }

    docs.push(Doc::hard_line());
    Doc::list(docs)
}

fn format_trivias_after_node<'a>(node: AmberNode<'a>, parent: AmberNode<'a>, ctx: &Ctx) -> Vec<Doc<'a>> {
    let mut tokens = parent
        .children_with_tokens()
        .skip_while(|node_or_token| node_or_token.text_range().start() <= node.text_range().start())
        .map_while(NodeOrToken::into_token)
        .peekable();
    let mut trivias = Vec::with_capacity(1);
    while let Some(token) = tokens.next() {
        match token.kind() {
            SyntaxKind::LINE_COMMENT
            | SyntaxKind::BLOCK_COMMENT
            | SyntaxKind::ERROR
            | SyntaxKind::ANNOT_START
            | SyntaxKind::ANNOT_ELEM
            | SyntaxKind::ANNOT_END => trivias.push(token),
            SyntaxKind::WHITESPACE
                if tokens.peek().is_none_or(|token| match token.kind() {
                    SyntaxKind::R_PAREN => false,
                    SyntaxKind::KEYWORD => token.text() != "end",
                    _ => true,
                }) =>
            {
                trivias.push(token);
            }
            _ => break,
        }
    }
    if trivias
        .iter()
        .all(|token| token.kind() == SyntaxKind::WHITESPACE && token.text().chars().filter(|c| *c == '\n').count() < 2)
    {
        return vec![];
    }
    let mut docs = Vec::with_capacity(trivias.len());
    if trivias.first().is_some_and(|token| token.kind().is_comment()) {
        docs.push(Doc::soft_line());
    }
    trivias.iter().for_each(|token| match token.kind() {
        SyntaxKind::LINE_COMMENT => {
            docs.push(format_line_comment(token.text(), ctx));
        }
        SyntaxKind::BLOCK_COMMENT => {
            docs.push(format_block_comment(token.text(), ctx));
        }
        SyntaxKind::WHITESPACE => match token.text().chars().filter(|c| *c == '\n').count() {
            0 => docs.push(Doc::space()),
            1 => docs.push(Doc::hard_line()),
            _ => {
                docs.push(Doc::empty_line());
                docs.push(Doc::hard_line());
            }
        },
        SyntaxKind::ERROR | SyntaxKind::ANNOT_START | SyntaxKind::ANNOT_ELEM | SyntaxKind::ANNOT_END => {
            docs.push(Doc::text(token.text()));
        }
        _ => {}
    });
    docs
}
fn format_trivias_after_token<'a>(token: AmberToken<'a>, parent: AmberNode<'a>, ctx: &Ctx) -> Vec<Doc<'a>> {
    let mut tokens = parent
        .children_with_tokens()
        .skip_while(|node_or_token| node_or_token.text_range().start() <= token.text_range().start())
        .map_while(NodeOrToken::into_token)
        .peekable();
    let mut trivias = Vec::with_capacity(1);
    while let Some(current) = tokens.next() {
        match current.kind() {
            SyntaxKind::LINE_COMMENT
            | SyntaxKind::BLOCK_COMMENT
            | SyntaxKind::ERROR
            | SyntaxKind::ANNOT_START
            | SyntaxKind::ANNOT_ELEM
            | SyntaxKind::ANNOT_END => trivias.push(current),
            SyntaxKind::WHITESPACE
                if tokens.peek().is_none_or(|token| match token.kind() {
                    SyntaxKind::R_PAREN => false,
                    SyntaxKind::KEYWORD => token.text() != "end",
                    _ => true,
                }) =>
            {
                if !(token.kind() == SyntaxKind::L_PAREN
                    && current.kind() == SyntaxKind::WHITESPACE
                    && trivias.is_empty())
                {
                    trivias.push(current);
                }
            }
            _ => break,
        }
    }
    if trivias.iter().all(|token| token.kind() == SyntaxKind::WHITESPACE) {
        return vec![];
    }
    let mut docs = Vec::with_capacity(trivias.len());
    trivias.iter().for_each(|token| match token.kind() {
        SyntaxKind::LINE_COMMENT => {
            docs.push(format_line_comment(token.text(), ctx));
        }
        SyntaxKind::BLOCK_COMMENT => {
            docs.push(format_block_comment(token.text(), ctx));
        }
        SyntaxKind::WHITESPACE => match token.text().chars().filter(|c| *c == '\n').count() {
            0 => docs.push(Doc::space()),
            1 => docs.push(Doc::hard_line()),
            _ => {
                docs.push(Doc::empty_line());
                docs.push(Doc::hard_line());
            }
        },
        SyntaxKind::ERROR | SyntaxKind::ANNOT_START | SyntaxKind::ANNOT_ELEM | SyntaxKind::ANNOT_END => {
            docs.push(Doc::text(token.text()));
        }
        _ => {}
    });
    docs
}

fn format_line_comment<'a>(text: &'a str, ctx: &Ctx) -> Doc<'a> {
    if ctx.options.format_comments {
        let content = text.strip_prefix(";;").expect("line comment must start with `;;`");
        if content.is_empty() || content.starts_with([' ', '\t']) {
            Doc::text(text)
        } else {
            Doc::text(format!(";; {content}"))
        }
    } else {
        Doc::text(text)
    }
}

fn format_block_comment<'a>(text: &'a str, ctx: &Ctx) -> Doc<'a> {
    if ctx.options.format_comments {
        let content = text
            .strip_prefix("(;")
            .and_then(|s| s.strip_suffix(";)"))
            .expect("block comment must be wrapped between `(;` and `;)`");
        let has_leading_ws = content.starts_with([' ', '\t']);
        let has_trailing_ws = content.ends_with([' ', '\t']);
        if content.is_empty() || has_leading_ws && has_trailing_ws {
            Doc::text(text)
        } else if has_leading_ws {
            Doc::text(format!("(;{content} ;)"))
        } else if has_trailing_ws {
            Doc::text(format!("(; {content};)"))
        } else {
            Doc::text(format!("(; {content} ;)"))
        }
    } else {
        Doc::text(text)
    }
}

fn reflow(text: String, docs: &mut Vec<Doc>) {
    let mut lines = text.lines();
    if let Some(line) = lines.next() {
        docs.push(Doc::text(line.to_owned()));
    }
    for line in lines {
        docs.push(Doc::empty_line());
        docs.push(Doc::text(line.to_owned()));
    }
}

fn should_ignore(node: AmberNode, parent: AmberNode, ctx: &Ctx) -> bool {
    parent
        .children_with_tokens()
        .rev()
        .skip_while(|node_or_token| node_or_token.text_range().start() >= node.text_range().start())
        .map_while(NodeOrToken::into_token)
        .nth(1)
        .and_then(|token| {
            if token.kind() == SyntaxKind::LINE_COMMENT {
                token
                    .text()
                    .strip_prefix(";;")
                    .and_then(|s| s.trim_start().strip_prefix(&ctx.options.ignore_comment_directive))
            } else {
                None
            }
        })
        .is_some_and(|rest| rest.is_empty() || rest.starts_with(|c: char| c.is_ascii_whitespace()))
}

fn wrap_before<I>(children: &mut iter::Peekable<I>, option: WrapBefore) -> Doc<'static>
where
    I: Iterator,
{
    match option {
        WrapBefore::Never => Doc::space(),
        WrapBefore::Overflow => Doc::soft_line(),
        WrapBefore::MultiOnly => {
            if children.peek().is_some() {
                Doc::hard_line()
            } else {
                Doc::space()
            }
        }
        WrapBefore::Always => Doc::hard_line(),
    }
}

fn whitespace_of_multi_line(option: MultiLine, first: Option<AmberNode>, parent: AmberNode) -> Doc<'static> {
    match option {
        MultiLine::Never => Doc::space(),
        MultiLine::Overflow => Doc::line_or_space(),
        MultiLine::Smart => {
            if first.is_some_and(|first| {
                parent
                    .children_with_tokens()
                    .skip_while(|node_or_token| node_or_token.text_range().start() <= first.text_range().start())
                    .map_while(NodeOrToken::into_token)
                    .any(|token| token.text().contains('\n'))
            }) {
                Doc::hard_line()
            } else {
                Doc::line_or_space()
            }
        }
        MultiLine::Wrap => Doc::soft_line(),
        MultiLine::Always => Doc::hard_line(),
    }
}