tsv_ts 0.8.0

TypeScript/JS formatter, type stripper and acorn-compatible parser — part of tsv
Documentation
// Pure AST analysis functions for TypeScript printer
//
// These functions analyze AST structures without needing Printer state.
// They determine formatting decisions like whether expressions need
// special handling, wrapping, or expansion.

use crate::ast;
use crate::printer::Printer;
use tsv_lang::Span;
use tsv_lang::doc::arena::DocId;

/// Skip past identifier characters (alphanumeric, `_`, `$`, non-ASCII) starting at `pos`,
/// stopping at `end`. Returns the position after the last one, or `pos` if none found.
///
/// ⚠️ Deliberately **not** the parser's lookahead class (`parser::scan`'s
/// `identifier_continues_at`), which resolves the five UTF-8 lead bytes that can begin
/// ECMAScript whitespace and so ends a name at an `<NBSP>`. This one takes every byte
/// `> 127` as part of the name, and that is sound here only because every caller uses the
/// result as the **start of a comment-gap scan** — the name's own text comes from the
/// node (`Printer::identifier_name_doc`), never from slicing to this offset. Widening or
/// narrowing a gap's start by a whitespace character can neither add nor drop a comment,
/// since whitespace ahead of a comment is inert to those scans.
///
/// A caller that wants this as a *decision* — "does the word end here?" — must ask
/// `parser::scan::is_word_at` instead; asking it here would read `[K in<NBSP>keyof T]` as
/// one identifier, which is the bug that class exists to prevent.
pub(crate) fn skip_identifier_at(bytes: &[u8], pos: usize, end: usize) -> usize {
    let mut i = pos;
    while i < end
        && (bytes[i].is_ascii_alphanumeric()
            || bytes[i] == b'_'
            || bytes[i] == b'$'
            || bytes[i] > 127)
    {
        i += 1;
    }
    i
}

/// Whether a `{ }`-delimited statement list prints no visible content.
///
/// A body containing only standalone `EmptyStatement`s (bare `;`) prints
/// nothing — Prettier's `printStatementSequence` drops them — so it's treated
/// the same as a genuinely empty body (comments attached to those statements
/// are picked up separately, by scanning the full brace range rather than the
/// statement list). Used by block-statement and namespace/module bodies to
/// decide between the empty-body and normal rendering paths.
pub(crate) fn is_effectively_empty_body(body: &[ast::Statement<'_>]) -> bool {
    body.iter()
        .all(|s| matches!(&s.kind, ast::StatementKind::EmptyStatement(_)))
}

/// The start of the next statement after `index` that will actually be printed
/// — the first one that isn't a dropped standalone `EmptyStatement` — falling
/// back to `body_end` when only dropped statements (or nothing) follow.
///
/// Used to bound a statement's trailing same-line comment scan. A comment
/// physically trailing a dropped `;` on the statement's own line (`a();; // c`)
/// must attach to that statement: the `;` it follows emits nothing, so bounding
/// the scan at the `;` — rather than the next *printed* statement — would strand
/// the comment (neither the statement's trailing scan nor the dropped `;`'s
/// leading-comment collection claims it) and silently drop it.
pub(crate) fn next_printed_stmt_start(
    body: &[ast::Statement<'_>],
    index: usize,
    body_end: u32,
) -> u32 {
    next_printed_stmt(body, index).map_or(body_end, |s| s.span().start)
}

/// The next statement after `body[index]` that `printStatementSequence` will actually
/// print — `None` when only dropped `EmptyStatement`s (or nothing) follow. The
/// `Option` form of [`next_printed_stmt_start`], for the callers whose behavior
/// differs when nothing prints (the trailing-claim split runs only toward a printed
/// statement — a comment hugging the body's `}` still trails).
pub(crate) fn next_printed_stmt<'s, 'arena>(
    body: &'s [ast::Statement<'arena>],
    index: usize,
) -> Option<&'s ast::Statement<'arena>> {
    body[index + 1..]
        .iter()
        .find(|s| !matches!(&s.kind, ast::StatementKind::EmptyStatement(_)))
}

/// The slot floor for `body[index]`'s trailing gap: past the last dropped
/// `EmptyStatement` between it and the next printed statement, else the statement's
/// own end. A comment before a dropped `;` binds inside its own slot and trails the
/// content before it (`a(); /* c */ ; b();` — prettier reads it the same way), so the
/// leads-next scan ([`Printer::trailing_claim_end`](super::Printer::trailing_claim_end))
/// must not reach back across one.
pub(crate) fn statement_gap_floor(body: &[ast::Statement<'_>], index: usize) -> u32 {
    body[index + 1..]
        .iter()
        .take_while(|s| matches!(&s.kind, ast::StatementKind::EmptyStatement(_)))
        .last()
        .map_or_else(|| body[index].span().end, |s| s.span().end)
}

/// Check if an expression is a module path call that should use fluid assignment wrapping
/// (break after `=` if too long, keeping the call together).
///
/// Patterns:
/// - `require.resolve(stringLiteral)`
/// - `await import(stringLiteral)`
pub(crate) fn is_module_path_fluid_call(expr: &ast::Expression<'_>, source: &str) -> bool {
    // Check for `await import(string)` — single-arg only (no options)
    if let ast::ExpressionKind::AwaitExpression(await_expr) = &expr.kind
        && let ast::ExpressionKind::ImportExpression(import_expr) = &await_expr.argument.kind
        && import_expr.options.is_none()
    {
        return is_string_literal(import_expr.source);
    }

    let ast::ExpressionKind::CallExpression(call) = &expr.kind else {
        return false;
    };

    // Must have exactly 1 argument that is a string literal
    if call.arguments.len() != 1 || !is_string_literal(call.arguments[0]) {
        return false;
    }

    // Check for `require.resolve()`
    if let ast::ExpressionKind::MemberExpression(member) = &call.callee.kind
        && !member.computed
        && !member.optional
        && let ast::ExpressionKind::Identifier(resolve_id) = &member.property.kind
        && resolve_id.name(source) == "resolve"
        && let ast::ExpressionKind::Identifier(require_id) = &member.object.kind
        && require_id.name(source) == "require"
    {
        return true;
    }

    false
}

/// Check if an expression is a string literal
pub(crate) fn is_string_literal(expr: &ast::Expression<'_>) -> bool {
    matches!(
        &expr.kind,
        ast::ExpressionKind::Literal(lit) if matches!(lit.value, ast::LiteralValue::String { .. })
    )
}

/// Check if a ConditionalExpression needs break-after-operator layout.
///
/// Prettier uses "break-after-operator" when the ternary's test expression
/// is binaryish (BinaryExpression or LogicalExpression). This causes the pattern:
/// ```typescript
/// const value =
///     condition === 'value'
///         ? consequent
///         : alternate;
/// ```
///
/// Prettier ref: `shouldBreakAfterOperator` (`print/assignment.js`)
pub fn conditional_should_break_after_op(expr: &ast::Expression<'_>) -> bool {
    if let ast::ExpressionKind::ConditionalExpression(cond) = &expr.kind {
        // Check if test is binaryish (BinaryExpression includes logical operators like &&, ||),
        // but exclude logical expressions with inline-able RHS (non-empty object/array).
        // Prettier ref: `shouldBreakAfterOperator` (`print/assignment.js`)
        // `isBinaryish(test) && !shouldInlineLogicalExpression(test)`
        if let ast::ExpressionKind::BinaryExpression(binary) = &cond.test.kind {
            !super::expressions::assignment::should_inline_logical_expression(binary)
        } else {
            false
        }
    } else {
        false
    }
}

/// Check if an expression is a multiline string literal (contains line continuations).
///
/// Strings with `\<newline>` need fluid layout because:
/// 1. They span multiple lines in source
/// 2. Prettier always wraps the declaration for these
pub(crate) fn is_multiline_string_literal(expr: &ast::Expression<'_>, source: &str) -> bool {
    if let ast::ExpressionKind::Literal(lit) = &expr.kind
        && let ast::LiteralValue::String { .. } = &lit.value
    {
        let raw = lit.span.extract(source);
        // Check for line continuation: backslash followed by newline
        raw.contains("\\\n") || raw.contains("\\\r")
    } else {
        false
    }
}

/// Check if a brace-delimited block was written as multiline in source
///
/// Detects newline immediately after opening brace: `{\n  ...}` vs `{ ... }`
/// Used for type literals, mapped types, and other brace-delimited constructs.
pub(crate) fn is_brace_block_multiline(source: &str, span: Span) -> bool {
    let source_text = span.extract(source);
    let after_brace = source_text.strip_prefix('{').unwrap_or("");
    after_brace.starts_with('\n')
        || after_brace.starts_with("\r\n")
        || after_brace.trim_start_matches(' ').starts_with('\n')
}

/// Context for object pattern expansion decisions
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum PatternContext {
    /// Pattern in function parameter position
    FunctionParameter,
    /// Pattern in standalone context (variable declaration, assignment)
    Standalone,
    /// The left of a destructuring default (`{ a } = …`). Prettier's `shouldBreak`
    /// excludes an `AssignmentPattern` parent (object.js), so such a pattern never
    /// expands on nesting — `{ a: { b } = {} }` stays inline (width-based breaking
    /// still applies elsewhere).
    AssignmentDefault,
}

/// Check if an object pattern should expand (print across multiple lines).
///
/// Prettier expands object patterns when any property has a nested pattern
/// (ObjectPattern or ArrayPattern) as its value. The object EXPRESSION has no
/// counterpart — it expands on its own authored newline and on whatever its
/// contents force, never on a nesting count.
///
/// Prettier expands based on nesting depth and context:
/// - In function parameters: depth 3+ expands (e.g., {a: {b}} stays, {a: {b: {c}}} expands)
/// - In standalone contexts: depth 2+ expands (e.g., {a: {b}} expands)
pub(crate) fn object_pattern_should_expand(
    obj: &ast::ObjectPattern<'_>,
    context: PatternContext,
) -> bool {
    let depth = pattern_nesting_depth(obj);
    match context {
        PatternContext::FunctionParameter => depth >= 3,
        PatternContext::Standalone => depth >= 2,
        // A destructuring default's left never expands on nesting (prettier
        // excludes AssignmentPattern parents from shouldBreak).
        PatternContext::AssignmentDefault => false,
    }
}

/// Calculate the maximum nesting depth of an object pattern
/// Depth 1 = simple pattern like {a}
/// Depth 2 = one level of nesting like {a: {b}}
/// Depth 3 = two levels of nesting like {a: {b: {c}}}
fn pattern_nesting_depth(obj: &ast::ObjectPattern<'_>) -> usize {
    let mut max_depth = 1;

    for prop in obj.properties {
        match prop {
            ast::ObjectPatternProperty::Property(p) => {
                let nested_depth = match &p.value.kind {
                    ast::ExpressionKind::ObjectPattern(nested_obj) => {
                        1 + pattern_nesting_depth(nested_obj)
                    }
                    ast::ExpressionKind::ArrayPattern(nested_arr) => {
                        1 + array_pattern_nesting_depth(nested_arr)
                    }
                    // A property with a DEFAULT (`x: { y } = …`) does NOT count its
                    // nested pattern toward the expansion depth — prettier's shouldBreak
                    // fires only on a value that is *directly* an Object/Array pattern and
                    // excludes an `AssignmentPattern` parent (object.js), so
                    // `{ a: { b } = {} }` stays inline. Treat it as depth 1, same as a
                    // plain binding (the `_ => 1` arm). (Surfaced by cargo-mutants: the
                    // recurse-into-`ap.left` arms survived because no fixture covered a
                    // defaulted nested pattern — the survivor was a real over-expansion bug.)
                    _ => 1,
                };
                max_depth = max_depth.max(nested_depth);
            }
            ast::ObjectPatternProperty::RestElement(_) => {}
        }
    }

    max_depth
}

/// Calculate the maximum nesting depth of an array pattern
fn array_pattern_nesting_depth(arr: &ast::ArrayPattern<'_>) -> usize {
    let mut max_depth = 1;

    for elem in arr.elements.iter().flatten() {
        let nested_depth = match &elem.kind {
            ast::ExpressionKind::ObjectPattern(nested_obj) => 1 + pattern_nesting_depth(nested_obj),
            ast::ExpressionKind::ArrayPattern(nested_arr) => {
                1 + array_pattern_nesting_depth(nested_arr)
            }
            // A defaulted element (`[{ y } = …]`) doesn't count its nested pattern
            // toward the expansion depth — see the object-pattern counterpart above
            // (prettier's shouldBreak excludes `AssignmentPattern` parents).
            _ => 1,
        };
        max_depth = max_depth.max(nested_depth);
    }

    max_depth
}

/// Check if a template literal contains newlines in its content
///
/// Matches Prettier's `templateLiteralHasNewLines` function
pub(crate) fn template_literal_has_newlines(template: &ast::TemplateLiteral<'_>) -> bool {
    template.quasis.iter().any(|q| q.has_newline)
}

/// Check if an expression is a template literal (or tagged template) with embedded newlines.
///
/// Combines the TemplateLiteral/TaggedTemplateExpression dispatch with the newline check.
/// Used by call_formatting, new_expression, and arrow body formatting.
pub(crate) fn is_multiline_template_expression(expr: &ast::Expression<'_>) -> bool {
    match &expr.kind {
        ast::ExpressionKind::TemplateLiteral(t) => template_literal_has_newlines(t),
        ast::ExpressionKind::TaggedTemplateExpression(t) => template_literal_has_newlines(&t.quasi),
        _ => false,
    }
}

/// Build doc for TSEntityName (qualified names like `A.B.C`)
///
/// Needs the printer for the name-emission seam (span-identity source slices,
/// arena-string escaped names).
pub(crate) fn build_entity_name_doc(printer: &Printer<'_>, name: &ast::TSEntityName<'_>) -> DocId {
    match name {
        ast::TSEntityName::Identifier(id) => printer.identifier_name_doc(id),
        // A qualified name is a dotted pair, so both gaps around the `.` are positions an
        // author can comment in (`ns /* c */.Type`, `ns./* c */ Type`). The shared printer
        // emits them; concatenating the three pieces scans neither and drops what's there.
        // One printer serves every qualified name, so this reaches type references,
        // interface heritage, and import-equals module references alike.
        ast::TSEntityName::QualifiedName(qn) => printer.build_dotted_pair_doc(
            build_entity_name_doc(printer, &qn.left),
            printer.identifier_name_doc(&qn.right),
            qn.left.span().end,
            qn.right.span.start,
        ),
    }
}