shuck-formatter 0.0.45

Shell script formatter with configurable style options
Documentation
use rustc_hash::FxHashMap as HashMap;

use shuck_ast::Span;

use crate::comments::{BranchPrefixComment, CommentAttachmentModel, SourceComment, SourceMap};
use crate::streaming::comments_alignment::CommentAlignmentFacts;

use super::{FactSpan, gap_has_empty_physical_line, line_end_for_offset};

#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
struct OffsetRegionKey {
    start: usize,
    end: usize,
}

impl OffsetRegionKey {
    fn new(start: usize, end: usize) -> Self {
        Self { start, end }
    }
}

#[derive(Debug, Clone)]
pub(crate) struct BranchPrefixFacts {
    comments: Vec<BranchPrefixComment>,
    first_comment_offset: Option<usize>,
    has_blank_line_before_keyword: bool,
    has_blank_line_after_comments: bool,
}

impl BranchPrefixFacts {
    pub(super) fn new(
        source: &str,
        start: usize,
        end: usize,
        comments: Vec<BranchPrefixComment>,
    ) -> Self {
        let first_comment_offset = comments.first().map(|comment| comment.offset);
        let has_blank_line_before_keyword =
            gap_has_empty_physical_line(source, start, first_comment_offset.unwrap_or(end));
        let has_blank_line_after_comments = comments.last().is_some_and(|last| {
            line_end_for_offset(source, last.offset)
                .filter(|line_end| *line_end < end)
                .is_some_and(|line_end| gap_has_empty_physical_line(source, line_end, end))
        });

        Self {
            comments,
            first_comment_offset,
            has_blank_line_before_keyword,
            has_blank_line_after_comments,
        }
    }

    pub(crate) fn comments(&self) -> &[BranchPrefixComment] {
        &self.comments
    }

    pub(crate) fn first_comment_offset(&self) -> Option<usize> {
        self.first_comment_offset
    }

    pub(crate) fn has_blank_line_before_keyword(&self) -> bool {
        self.has_blank_line_before_keyword
    }

    pub(crate) fn has_blank_line_after_comments(&self) -> bool {
        self.has_blank_line_after_comments
    }
}

#[derive(Debug, Clone)]
pub(super) struct CommentFacts<'source> {
    alignment: HashMap<FactSpan, CommentAlignmentFacts>,
    suffix_comments: HashMap<FactSpan, SourceComment<'source>>,
    close_suffix_comments: HashMap<FactSpan, SourceComment<'source>>,
    branch_prefix_facts: HashMap<OffsetRegionKey, BranchPrefixFacts>,
}

impl<'source> CommentFacts<'source> {
    pub(super) fn new(
        source: &'source str,
        source_map: &SourceMap<'source>,
        comment_attachments: &CommentAttachmentModel<'source>,
    ) -> Self {
        let alignment = comment_attachments
            .comments()
            .iter()
            .map(|comment| {
                (
                    FactSpan::from(comment.span()),
                    CommentAlignmentFacts::new(source, source_map, comment),
                )
            })
            .collect();

        Self {
            alignment,
            suffix_comments: HashMap::default(),
            close_suffix_comments: HashMap::default(),
            branch_prefix_facts: HashMap::default(),
        }
    }

    pub(super) fn trailing_comment(
        &self,
        source_map: &SourceMap<'source>,
        comment: SourceComment<'source>,
    ) -> InlineCommentPlan<'source> {
        InlineCommentPlan::new(
            comment,
            self.alignment_for(source_map, comment),
            InlineCommentPlacement::Trailing,
        )
    }

    pub(super) fn suffix_comment_for_span(
        &self,
        source_map: &SourceMap<'source>,
        span: Span,
    ) -> Option<InlineCommentPlan<'source>> {
        self.suffix_comments
            .get(&FactSpan::from(span))
            .copied()
            .map(|comment| self.trailing_comment(source_map, comment))
    }

    pub(super) fn close_suffix_comment_after_span(
        &self,
        source_map: &SourceMap<'source>,
        span: Span,
    ) -> Option<InlineCommentPlan<'source>> {
        let comment = self
            .close_suffix_comments
            .get(&FactSpan::from(span))
            .copied()
            .or_else(|| source_map.suffix_comment_after_span(span))?;
        Some(InlineCommentPlan::new(
            comment,
            self.alignment_for(source_map, comment),
            InlineCommentPlacement::CloseSuffix,
        ))
    }

    pub(super) fn branch_prefix_facts(
        &self,
        start: usize,
        end: usize,
    ) -> Option<&BranchPrefixFacts> {
        self.branch_prefix_facts
            .get(&OffsetRegionKey::new(start, end))
    }

    pub(super) fn insert_suffix_comment(&mut self, span: Span, comment: SourceComment<'source>) {
        self.suffix_comments.insert(FactSpan::from(span), comment);
    }

    pub(super) fn insert_close_suffix_comment(
        &mut self,
        span: Span,
        comment: SourceComment<'source>,
    ) {
        self.close_suffix_comments
            .insert(FactSpan::from(span), comment);
    }

    pub(super) fn insert_branch_prefix_facts(
        &mut self,
        start: usize,
        end: usize,
        facts: BranchPrefixFacts,
    ) {
        self.branch_prefix_facts
            .insert(OffsetRegionKey::new(start, end), facts);
    }

    fn alignment_for(
        &self,
        source_map: &SourceMap<'source>,
        comment: SourceComment<'source>,
    ) -> CommentAlignmentFacts {
        self.alignment
            .get(&FactSpan::from(comment.span()))
            .copied()
            .unwrap_or_else(|| {
                CommentAlignmentFacts::new(source_map.source(), source_map, &comment)
            })
    }

    #[cfg(feature = "benchmarking")]
    pub(super) fn len(&self) -> usize {
        self.alignment.len()
            + self.suffix_comments.len()
            + self.close_suffix_comments.len()
            + self.branch_prefix_facts.len()
    }
}

#[derive(Debug, Clone, Copy)]
pub(crate) struct InlineCommentPlan<'source> {
    comment: SourceComment<'source>,
    alignment: CommentAlignmentFacts,
    placement: InlineCommentPlacement,
}

impl<'source> InlineCommentPlan<'source> {
    fn new(
        comment: SourceComment<'source>,
        alignment: CommentAlignmentFacts,
        placement: InlineCommentPlacement,
    ) -> Self {
        Self {
            comment,
            alignment,
            placement,
        }
    }

    pub(crate) fn comment(self) -> SourceComment<'source> {
        self.comment
    }

    pub(crate) fn padding(
        self,
        source_map: &SourceMap<'_>,
        current_code_column: usize,
        current_indent_column: usize,
    ) -> usize {
        match self.placement {
            InlineCommentPlacement::Trailing => self.alignment.trailing_padding(
                source_map.source(),
                source_map,
                &self.comment,
                current_code_column,
                current_indent_column,
            ),
            InlineCommentPlacement::CloseSuffix => self.alignment.close_suffix_padding(
                source_map.source(),
                source_map,
                &self.comment,
                current_code_column,
                current_indent_column,
            ),
        }
    }

    pub(crate) fn has_alignment(
        self,
        source_map: &SourceMap<'_>,
        current_indent_column: usize,
    ) -> bool {
        self.alignment.has_trailing_alignment(
            source_map.source(),
            source_map,
            &self.comment,
            current_indent_column,
        )
    }
}

#[derive(Debug, Clone, Copy)]
enum InlineCommentPlacement {
    Trailing,
    CloseSuffix,
}