tsv_ts 0.8.0

TypeScript/JS formatter, type stripper and acorn-compatible parser — part of tsv
Documentation
// Chain helpers on the main Printer for the chain renderer.
//
// These let the chain builder/printer delegate back to the main Printer for
// expression building and comment handling. Formerly the `ChainPrinter` /
// `SymbolLookup` traits — collapsed to inherent methods, since `Printer` was
// their only implementor.

use super::printing::ComputedBracket;
use super::types::ChainCall;
use crate::ast;
use crate::printer::comments::CommentVec;
use crate::printer::{CommentSpacing, Printer};
use tsv_lang::doc::DocBuf;
use tsv_lang::doc::arena::{DocArena, DocId};
use tsv_lang::{ClassifiedComments, Comment, Span};

impl<'a> Printer<'a> {
    pub(crate) fn arena(&self) -> &DocArena {
        self.arena
    }

    pub(crate) fn ident_doc(&self, name: ast::IdentName<'_>, name_start: u32) -> DocId {
        self.ident_name_doc(name, name_start)
    }

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

    pub(crate) fn print_call_args(
        &self,
        call: &ast::CallExpression<'_>,
        span: Span,
        facts: ChainCall,
    ) -> DocId {
        self.build_call_args_doc_for_chain(call, span, facts)
    }

    pub(crate) fn print_call_args_expanded(
        &self,
        call: &ast::CallExpression<'_>,
        span: Span,
        facts: ChainCall,
    ) -> DocId {
        self.build_call_args_doc_for_chain_expanded(call, span, facts)
    }

    pub(crate) fn build_chain_block_comments_doc(
        &self,
        start: u32,
        end: u32,
        spacing: CommentSpacing,
    ) -> DocId {
        let block_comments: CommentVec<'_> = self
            .comments_to_emit_between(start, end)
            .filter(|c| c.is_block)
            .collect();
        self.format_block_comments(&block_comments, spacing)
    }

    pub(crate) fn build_computed_member_line_comment_bracket(
        &self,
        open: &'static str,
        bracket: ComputedBracket,
        prop_start: u32,
        prop_end: u32,
        bracket_end: u32,
        inner: DocId,
    ) -> Option<DocId> {
        let inside_start = bracket.inside;
        // Only the break path — a line comment before the index or after it (before
        // `]`). A block-only or comment-free bracket falls through to the caller.
        let body_trailing_line_comment = self.has_line_comments_between(prop_end, bracket_end);
        if !self.has_line_comments_between(inside_start, prop_start) && !body_trailing_line_comment
        {
            return None;
        }
        let d = self.d();
        // Build the body (index + any index→`]` trailing comments), then hand it to the
        // shared bracket-break helper (it owns the `[`→index line-comment prefix and the
        // break shell, mirroring the computed-key bracket). `[`→index: a `[`-line comment
        // is pulled onto the `[` line, an own-line one keeps its line (blank-preserving).
        // index→`]`: a same-line comment trails the index, an own-line one keeps its line.
        let mut body_parts = DocBuf::new();
        body_parts.push(inner);
        if self.has_comments_to_emit_between(prop_end, bracket_end) {
            self.push_deferred_run_end(&mut body_parts, inner);
        }
        let mut prev = prop_end;
        for comment in self.comments_to_emit_between(prop_end, bracket_end) {
            if self.is_same_line(prev, comment.span.start) {
                body_parts.push(d.text(" "));
            } else {
                body_parts.push(d.hardline());
            }
            body_parts.push(self.build_comment_doc(comment));
            prev = comment.span.end;
        }
        Some(self.build_bracket_line_comment_break(
            open,
            bracket.bracket_pos(),
            prop_start,
            body_trailing_line_comment,
            d.concat(&body_parts),
        ))
    }

    pub(crate) fn get_property_span(&self, expr: &ast::Expression<'_>) -> Span {
        expr.span()
    }

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

    pub(crate) fn clear_expression_statement(&self) {
        self.is_expression_statement.set(false);
    }

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

    pub(crate) fn set_chain_has_comments(&self, has_comments: bool) -> bool {
        let prev = self.chain_has_comments.get();
        self.chain_has_comments.set(has_comments);
        prev
    }

    pub(crate) fn restore_chain_has_comments(&self, prev: bool) {
        self.chain_has_comments.set(prev);
    }

    /// The chain gaps' one classification seam — every chain consumer (the builder's
    /// gap ownership, the dot/computed print paths, `push_gap_comments_and_break`)
    /// classifies through here, so they take the ADVANCING trailing anchor together
    /// and the gap's two sides cannot drift into a double-print
    /// (`ClassifiedComments::from_index_advancing` — see its ⚠️ for why the advance
    /// is not `from_range`'s one behavior).
    pub(crate) fn classify_comments(&self, start: u32, end: u32) -> ClassifiedComments<'_> {
        ClassifiedComments::from_index_advancing(
            self.comment_free_gap.comments(),
            self.first_index_between(start, end),
            start,
            end,
            self.source.as_bytes(),
            self.comment_line_breaks,
        )
    }

    /// A chain gap's trail scan opening at `start` in its next-pass reading
    /// ([`tsv_lang::GapTrailScan::fixed`]) — for a predicate that walks the gap itself (a
    /// different axis, an early exit) and reads it as the NEXT pass will, where a comment
    /// on a multi-line block's closing line stands on a line the block's own newline
    /// began. It shares the emitter's `)` arm ([`Self::classify_comments`] walks
    /// [`tsv_lang::GapTrailScan::new`]), so neither can read a comment printed after a
    /// kept pair's `)` as own-line. Feed it the gap's comments in source order through
    /// [`Self::chain_gap_trail`].
    pub(crate) const fn chain_gap_trail_scan_fixed(&self, start: u32) -> tsv_lang::GapTrailScan {
        tsv_lang::GapTrailScan::fixed(start)
    }

    /// A chain gap's trail scan opening at `start` in the emitter's own reading
    /// ([`tsv_lang::GapTrailScan::new`], the scan [`Self::classify_comments`] walks).
    pub(crate) const fn chain_gap_trail_scan(&self, start: u32) -> tsv_lang::GapTrailScan {
        tsv_lang::GapTrailScan::new(start)
    }

    /// How `comment`, the next comment of the gap `scan` walks, stands — see
    /// [`tsv_lang::GapTrailScan`].
    pub(crate) fn chain_gap_trail(
        &self,
        scan: &mut tsv_lang::GapTrailScan,
        comment: &Comment,
    ) -> tsv_lang::GapTrail {
        scan.read(comment, self.source.as_bytes(), self.comment_line_breaks)
    }

    /// A chain node's gap classification, honoring the node's own
    /// [`super::ChainNode::paren_gap_skip`] hole.
    ///
    /// The common node has no hole and this is exactly [`Self::classify_comments`]. A
    /// node whose gap was widened back over a stripped grouping paren has one, and its
    /// claim is the two regions either side of it — the paren prefix and its own gap —
    /// classified separately so each is read against its own anchor.
    ///
    /// ⚠️ **Every layout predicate asked about the same gap must honor the hole too**, and
    /// they all do it through one seam — [`super::printing::chain_gap_any`], whose ⚠️ carries
    /// the argument. This emitter keeps its own spelling only because it builds a
    /// `ClassifiedComments` rather than answering a `bool`, and an unconditional second
    /// classify would cost every hole-less gap (the common case) for nothing.
    pub(crate) fn classify_chain_gap(
        &self,
        start: u32,
        end: u32,
        skip: Option<Span>,
    ) -> ClassifiedComments<'_> {
        let Some(skip) = skip else {
            return self.classify_comments(start, end);
        };
        let mut classified = self.classify_comments(start, skip.start);
        classified.append(self.classify_comments(skip.end, end));
        classified
    }

    /// A gap's same-line block run, each comment behind a space
    /// (`method() /* c */`). The named entry for
    /// [`format_block_comments`](Self::format_block_comments) at `Leading` spacing,
    /// so the spacing rule lives once.
    pub(crate) fn build_trailing_block_doc(&self, comments: &[&Comment]) -> DocId {
        self.format_block_comments(comments, CommentSpacing::Leading)
    }

    /// A chain gap's trailing line comments, deferred via `line_suffix` — the run
    /// rides to the end of the output line and is flushed by whatever break the
    /// caller emits next.
    ///
    /// **The caller must emit that break.** A `line_suffix_boundary` here would flush
    /// the run *and end the line itself* (see `tsv_lang`'s `render_line_node`), so a
    /// caller that also breaks — every chain gap does, `build_chain_line_break` being
    /// a hardline at minimum — would render a blank line under the comment. Prettier
    /// plants its boundary at a member lookup whose following `softline` may collapse
    /// (`print/member.js`); a chain gap is the case where it may not.
    pub(crate) fn build_deferred_line_comments_doc(&self, comments: &[&Comment]) -> DocId {
        let d = self.d();
        if comments.is_empty() {
            return d.empty();
        }

        let mut parts = DocBuf::with_capacity(comments.len());
        for comment in comments {
            parts.push(self.build_trailing_line_comment_doc(comment));
        }
        d.concat(&parts)
    }

    pub(crate) fn get_source(&self) -> &str {
        self.source
    }
}

impl<'a> Printer<'a> {
    /// Format an already-filtered slice of **block** comments, each with `spacing`
    /// applied to its outer edges (` /* c */` after a chain element, `/* c */ ` before
    /// one). Every comment gets the space — a chain gap's block run is never at line
    /// start — so this is the plain-`true` consumer of
    /// [`Printer::push_block_comment_spaced`], which owns what the spacing means.
    pub(crate) fn format_block_comments(
        &self,
        block_comments: &[&Comment],
        spacing: CommentSpacing,
    ) -> DocId {
        let d = self.d();
        if block_comments.is_empty() {
            return d.empty();
        }

        let mut parts = DocBuf::new();
        for comment in block_comments {
            self.push_block_comment_spaced(&mut parts, comment, spacing, true);
        }
        d.concat(&parts)
    }
}