Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 3 additions & 4 deletions crates/tsv_svelte/src/ast/convert/special.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@ use tsv_lang::{
Comment, JsonWriter, LocationMapper, LocationTracker, Position, Span, estimated_json_capacity,
};
use tsv_ts::ast::convert::{
CommentMode, Schema, SkeletonRecorder, SkeletonTree, WriterComments, write_expression_embedded,
write_program_embedded, write_variable_declaration_embedded,
CommentMode, ProgramLoc, Schema, SkeletonRecorder, SkeletonTree, WriterComments,
write_expression_embedded, write_program_embedded, write_variable_declaration_embedded,
};

use super::comment_attachment::{
Expand Down Expand Up @@ -243,9 +243,8 @@ pub(super) fn build_script_writer_comments(
source,
LocationMapper::identity(tracker),
schema,
(dummy, dummy),
ProgramLoc::Emit(dummy, dummy), // skeleton pass: bytes discarded, loc irrelevant
CommentMode::Record(&recorder),
true, // skeleton pass: bytes discarded, loc irrelevant
);
let tree = recorder.finish();
let root = tree.roots()[0];
Expand Down
18 changes: 8 additions & 10 deletions crates/tsv_svelte/src/ast/convert/write.rs
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ use tsv_lang::{
write_array, write_or_null,
};
use tsv_ts::ast::convert::{
CommentMode, Schema, translate_column, write_expression_embedded,
CommentMode, ProgramLoc, Schema, translate_column, write_expression_embedded,
write_identifier_expression_with_character, write_pattern_embedded, write_program_embedded,
write_variable_declaration_embedded,
};
Expand Down Expand Up @@ -1496,27 +1496,25 @@ fn write_script_program_fused(
) as usize,
}
};
// The override is only consumed when `loc` is emitted; on the no-locations
// path it's discarded, so skip the two `get_line_column` line-table lookups
// (which would only hit the stub `[0]` table anyway — see `new_map_only`).
let loc_override = if ctx.emit_loc {
(
// The override is only consumed when `loc` is emitted; `ProgramLoc::Omit` is
// the no-locations path, which skips the two `get_line_column` line-table
// lookups (which would only hit the stub `[0]` table anyway — see `new_map_only`).
let program_loc = if ctx.emit_loc {
ProgramLoc::Emit(
position_at(script.span.start, program.span.start),
position_at(script.span.end, program.span.end),
)
} else {
let dummy = Position { line: 1, column: 0 };
(dummy, dummy)
ProgramLoc::Omit
};
write_program_embedded(
w,
program,
ctx.source,
ctx.loc,
schema,
loc_override,
program_loc,
comments,
ctx.emit_loc,
);
}

Expand Down
2 changes: 1 addition & 1 deletion crates/tsv_ts/src/ast/convert/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ impl Schema {
mod write;

pub use write::{
AttachedComment, CommentMode, SkeletonRecorder, SkeletonTree, WriterComments,
AttachedComment, CommentMode, ProgramLoc, SkeletonRecorder, SkeletonTree, WriterComments,
write_expression_embedded, write_identifier_expression_with_character, write_pattern_embedded,
write_program_embedded, write_program_json, write_variable_declaration_embedded,
};
Expand Down
38 changes: 25 additions & 13 deletions crates/tsv_ts/src/ast/convert/write/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -258,38 +258,35 @@ fn write_program(
/// `<script>` block's `content` into its own buffer. Shares the host document's
/// `LocationMapper` (spans are host-file coordinates), threads the
/// `Schema`, and — unlike a standalone `Program` — emits the node's own `loc`
/// from `loc_override` rather than deriving it from `program.span`.
/// from `program_loc` rather than deriving it from `program.span`.
///
/// Svelte reports the `Program` `loc` against the `<script>` **tag** (start line,
/// column 0) and the tag's closing `</script>`, not the content span; the caller
/// supplies those two final char-space `Position`s (the offset-translated form of
/// Svelte's byte-space override). `start`/`end` offsets still come from
/// `program.span` via `loc.pos`, and the body/`sourceType` are emitted exactly as
/// the standalone program writer does — so an eligible (comment-free, `lang="ts"`,
/// no preceding HTML comment) script's `content` matches the standalone
/// `Program` emission.
#[allow(clippy::too_many_arguments)]
/// supplies those two final char-space `Position`s via `ProgramLoc::Emit` (the
/// offset-translated form of Svelte's byte-space override), or `ProgramLoc::Omit`
/// for the no-locations wire. `start`/`end` offsets still come from `program.span`
/// via `loc.pos`, and the body/`sourceType` are emitted exactly as the standalone
/// program writer does — so an eligible (comment-free, `lang="ts"`, no preceding
/// HTML comment) script's `content` matches the standalone `Program` emission.
pub fn write_program_embedded(
w: &mut JsonWriter,
program: &internal::Program<'_>,
source: &str,
loc: LocationMapper<'_>,
schema: Schema,
loc_override: (Position, Position),
program_loc: ProgramLoc,
comments: CommentMode<'_>,
emit_loc: bool,
) {
let mut ctx = Ctx::new(source, loc);
ctx.vanilla_acorn = schema.is_svelte_script();
ctx.comments = comments;
ctx.emit_loc = emit_loc;
ctx.emit_loc = matches!(program_loc, ProgramLoc::Emit(..));
record_open("Program", program.span, &ctx);
w.raw("{\"type\":\"Program\",\"start\":");
w.u32(loc.pos(program.span.start));
w.raw(",\"end\":");
w.u32(loc.pos(program.span.end));
if emit_loc {
let (start_pos, end_pos) = loc_override;
if let ProgramLoc::Emit(start_pos, end_pos) = program_loc {
w.raw(",\"loc\":{\"start\":{\"line\":");
w.usize(start_pos.line);
w.raw(",\"column\":");
Expand Down Expand Up @@ -323,6 +320,21 @@ pub enum CommentMode<'a> {
Record(&'a SkeletonRecorder),
}

/// The embedded `Program` node's `loc` source (see `write_program_embedded`).
///
/// Fuses the former `loc_override` + `emit_loc` parameters into one value so the
/// "no `loc` but a meaningful override" state is unrepresentable — the caller no
/// longer builds a dummy `Position` pair just to satisfy the signature. `Omit`
/// is the no-locations wire, which drops `loc` from every node globally (it sets
/// `Ctx::emit_loc`), this `Program` included.
#[derive(Clone, Copy)]
pub enum ProgramLoc {
/// No-locations wire: omit `loc` on the `Program` (and every node).
Omit,
/// Emit the `Program`'s `loc` from Svelte's tag-line `(start, end)` positions.
Emit(Position, Position),
}

/// The per-document environment every writer function shares (`source` and the
/// `LocationMapper`).
///
Expand Down
35 changes: 23 additions & 12 deletions crates/tsv_ts/src/printer/chain/printing.rs
Original file line number Diff line number Diff line change
Expand Up @@ -141,10 +141,12 @@ pub(crate) fn print_node_inner<'a>(
} => print_member_access(
printer,
*property,
*property_start,
MemberSpans {
name_start: *property_start,
object_end: *object_end,
property_start: *property_start,
},
*optional,
*object_end,
*property_start,
false,
skip_comments,
),
Expand All @@ -158,10 +160,12 @@ pub(crate) fn print_node_inner<'a>(
} => print_member_access(
printer,
*property,
*name_start,
MemberSpans {
name_start: *name_start,
object_end: *object_end,
property_start: *property_start,
},
*optional,
*object_end,
*property_start,
true,
skip_comments,
),
Expand Down Expand Up @@ -425,21 +429,28 @@ fn computed_lookup_doc(
///
/// The `skip_comments` flag is used by the expanded path where `add_comments_and_break`
/// already handles comments for the first member of rest groups.
#[allow(clippy::too_many_arguments)]
/// Source positions for a member-access chain node.
struct MemberSpans {
/// Start of the property name (a `#`-private name anchors past the `#`).
name_start: u32,
/// End of the object expression (start of the object→property gap).
object_end: u32,
/// Start of the property (end of the object→property gap).
property_start: u32,
}

fn print_member_access(
printer: &Printer<'_>,
property: internal::IdentName<'_>,
name_start: u32,
spans: MemberSpans,
optional: bool,
object_end: u32,
property_start: u32,
is_private: bool,
skip_comments: bool,
) -> DocId {
let d = printer.arena();
// Build member doc without format! allocation — span-identity source slice
// (or the escaped name's arena string)
let prop_doc = printer.ident_doc(property, name_start);
let prop_doc = printer.ident_doc(property, spans.name_start);
let member_doc = match (optional, is_private) {
(false, false) => d.concat(&[d.text("."), prop_doc]),
(true, false) => d.concat(&[d.text("?."), prop_doc]),
Expand All @@ -461,7 +472,7 @@ fn print_member_access(
}

// Classify all comments in the range between object and property
let classified = printer.classify_comments(object_end, property_start);
let classified = printer.classify_comments(spans.object_end, spans.property_start);

// Trailing block comments: same line as previous element (e.g., `method() /* c */.prop`)
let trailing_block = printer.build_trailing_block_doc(&classified.trailing_block);
Expand Down
69 changes: 54 additions & 15 deletions crates/tsv_ts/src/printer/class_common.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,50 @@ pub(in crate::printer) struct ClassHeritagePositions {
pub header_end: u32,
}

/// How `build_class_header_doc` lays out the class heritage.
///
/// Encodes the real 3-state decision that `(group_mode, has_heritage_line_comments)`
/// spelled as two booleans — the `(non-group, break)` combination is meaningless
/// (a non-group header never consults the break flag), so an enum makes it
/// unrepresentable.
pub(in crate::printer) enum ClassHeaderLayout {
/// Heritage stays inline; type params break independently (no unified group).
Independent,
/// One unified group — heritage breaks with the header on overflow.
Group,
/// A unified group forced open (a heritage line comment consumes its line).
GroupBreak,
}

impl ClassHeaderLayout {
/// Resolve the layout from the caller's group / heritage-line-comment flags.
/// `has_heritage_line_comments` is only meaningful in group mode.
pub(in crate::printer) fn from_flags(
group_mode: bool,
has_heritage_line_comments: bool,
) -> Self {
match (group_mode, has_heritage_line_comments) {
(false, _) => Self::Independent,
(true, false) => Self::Group,
(true, true) => Self::GroupBreak,
}
}
}

/// Non-content inputs to `build_class_header_doc` — the body's shape/position,
/// the heritage layout, and whether to emit header→body comments here.
pub(in crate::printer) struct ClassHeaderOptions {
/// The class body is `{}` — keep ` {}` on the heritage line (never `line()`).
pub body_is_empty: bool,
/// Start of the class body `{` — bounds the header→body comment scan.
pub body_start: u32,
/// How the heritage clauses lay out (group / independent / forced break).
pub layout: ClassHeaderLayout,
/// Emit header→body comments here (false when the caller already emitted the
/// bare name→body / anonymous→body comments).
pub emit_pre_body_comments: bool,
}

impl<'a> Printer<'a> {
/// Compute the heritage positions shared by both class printers.
pub(in crate::printer) fn class_heritage_positions(
Expand Down Expand Up @@ -275,24 +319,19 @@ impl<'a> Printer<'a> {
/// class-expression printer's bare name→body / anonymous→body paths emit
/// their own comments, so it passes `false` when there is no heritage or
/// type params (the declaration always passes `true`).
#[allow(clippy::too_many_arguments)]
pub(in crate::printer) fn build_class_header_doc(
&self,
mut parts: DocBuf,
positions: &ClassHeritagePositions,
extends_doc: Option<DocId>,
implements_doc: Option<DocId>,
implements: &[internal::TSInterfaceHeritage<'_>],
body_is_empty: bool,
body_start: u32,
group_mode: bool,
has_heritage_line_comments: bool,
emit_pre_body_comments: bool,
options: ClassHeaderOptions,
) -> DocId {
let d = self.d();
let header_end = positions.header_end;

if !group_mode {
if matches!(options.layout, ClassHeaderLayout::Independent) {
// Non-group mode: heritage stays inline, type params break independently.
if let Some(ext) = extends_doc {
parts.push(d.text(" "));
Expand All @@ -303,9 +342,9 @@ impl<'a> Printer<'a> {
parts.push(impl_doc);
}
parts.push(self.build_header_pre_body_doc(
emit_pre_body_comments,
options.emit_pre_body_comments,
header_end,
body_start,
options.body_start,
));
return d.concat(&parts);
}
Expand Down Expand Up @@ -351,23 +390,23 @@ impl<'a> Printer<'a> {
// Line comments force a hardline (they'd absorb the brace). For a
// non-empty body, `line()` puts the brace on its own line when the
// group breaks; an empty body always keeps ` {}` on the heritage line.
let has_line_comment =
emit_pre_body_comments && self.has_line_comments_between(header_end, body_start);
if emit_pre_body_comments
&& let Some(comments) = self.build_pre_body_comments_doc(header_end, body_start)
let has_line_comment = options.emit_pre_body_comments
&& self.has_line_comments_between(header_end, options.body_start);
if options.emit_pre_body_comments
&& let Some(comments) = self.build_pre_body_comments_doc(header_end, options.body_start)
{
parts.push(comments);
}
if has_line_comment {
parts.push(d.hardline());
} else if body_is_empty {
} else if options.body_is_empty {
parts.push(d.text(" "));
} else {
parts.push(d.line());
}

let parts_doc = d.concat(&parts);
if has_heritage_line_comments {
if matches!(options.layout, ClassHeaderLayout::GroupBreak) {
d.group_break(parts_doc)
} else {
d.group(parts_doc)
Expand Down
Loading