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
19 changes: 6 additions & 13 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,23 +11,16 @@ non-empty and carry a `<!-- bump: patch|minor|major -->` marker; `deno task publ
## Unreleased
<!-- bump: minor -->

- formatting is now non-configurable by design - tsv has no config files, CLI flags,
or runtime options, and none will be added
(this has no observable API changes because options had been deferred)
- support `format-ignore` as an alias to `prettier-ignore`
(along with `format-ignore-start` and `format-ignore-end` for templates)
- various conformance fixes to the formatter and parser
- numerous new Prettier divergences including uniform indentation on continuations
- object destructuring patterns in Svelte blocks now hug their braces,
consistent with `bracketSpacing: false` (which is not
respected by prettier-plugin-svelte even when set)
consistent with `bracketSpacing: false` (which is not respected by prettier-plugin-svelte)
- reduce allocations using `SmallVec` and memoizations
- formatting is now **non-configurable by design** — no config files, CLI flags,
or runtime options, and none are planned (opinionated like `gofmt` and Black).
Policy only — no change to formatter output or the published API
(`format_*` / `parse_*` / the `tsv` bin).
- parse AST: each comment now appears **once** in the public AST everywhere. tsv
previously replicated acorn-typescript's backtrack-and-reparse comment
duplication for type-space constructs (type literals, mapped/function types,
type assertions, type-member index/computed signatures, typed-param arrows);
it now corrects the duplication uniformly, matching the existing class-body
behavior. The set of distinct comments is unchanged; only the duplicate
entries are gone. Formatter output is unaffected.

## 0.1.0

Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,7 @@ Future features (unknown order):

- **[CLAUDE.md](CLAUDE.md)** - development guide (commands, structure, conventions)
- **[docs/architecture.md](docs/architecture.md)** - the major design decisions
- **[docs/directives.md](docs/directives.md)** - `format-ignore` / `prettier-ignore` formatting directives
- **[docs/conformance_prettier.md](docs/conformance_prettier.md)** - where formatting diverges from Prettier (and why)
- **[docs/conformance_svelte.md](docs/conformance_svelte.md)** - where the parser diverges from Svelte (and why)
- **[docs/conformance_test262.md](docs/conformance_test262.md)** - ECMAScript parser conformance
Expand Down
34 changes: 17 additions & 17 deletions crates/tsv_css/src/printer/atrules.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,9 @@
use super::Printer;
use super::value_normalization;
use crate::ast::internal;
use tsv_lang::comments_in_range;
use tsv_lang::doc::{self, Mode, arena::DocId};
use tsv_lang::{PRINT_WIDTH, TAB_WIDTH};
use tsv_lang::{comments_in_range, is_format_ignore_directive};

/// Convert a supports connector to its string representation
fn connector_str(conn: internal::SupportsConnector) -> &'static str {
Expand Down Expand Up @@ -177,7 +177,7 @@ impl<'a> Printer<'a> {
self.indent_level += 1;

let mut i = 0;
let mut prettier_ignore_next = false;
let mut format_ignore_next = false;
while i < block.children.len() {
let child = &block.children[i];

Expand Down Expand Up @@ -218,9 +218,9 @@ impl<'a> Printer<'a> {
internal::CssBlockChild::Rule(_) | internal::CssBlockChild::Atrule(_) => {
// Rules and at-rules need indentation
self.write_indent();
if prettier_ignore_next {
if format_ignore_next {
self.write(child.span().extract(self.source));
prettier_ignore_next = false;
format_ignore_next = false;
} else {
self.print_atrule_block_child(child);
}
Expand All @@ -231,9 +231,9 @@ impl<'a> Printer<'a> {
i += inline_count;
}
internal::CssBlockChild::Comment(comment) => {
// Check for prettier-ignore
if comment.content.trim() == "prettier-ignore" {
prettier_ignore_next = true;
// Check for a format-ignore directive
if is_format_ignore_directive(&comment.content) {
format_ignore_next = true;
}
// Standalone comment
self.write_indent();
Expand Down Expand Up @@ -295,7 +295,7 @@ impl<'a> Printer<'a> {
// Format declarations and comments with proper indentation
self.indent_level += 1;
let mut i = start_index;
let mut prettier_ignore_next = false;
let mut format_ignore_next = false;
while i < rule.declarations.len() {
let block_child = &rule.declarations[i];
match block_child {
Expand All @@ -306,14 +306,14 @@ impl<'a> Printer<'a> {
{
self.write("\n");
}
if prettier_ignore_next {
if format_ignore_next {
self.write_indent();
self.write(decl.span.extract(self.source));
if decl.is_important() {
self.write(" !important");
}
self.write(";\n");
prettier_ignore_next = false;
format_ignore_next = false;
} else {
self.print_css_declaration(decl);
}
Expand All @@ -338,9 +338,9 @@ impl<'a> Printer<'a> {
// Note: Previous element already ended with \n, so one more \n gives blank line
self.write("\n");
}
// Check for prettier-ignore
if comment.content.trim() == "prettier-ignore" {
prettier_ignore_next = true;
// Check for a format-ignore directive
if is_format_ignore_directive(&comment.content) {
format_ignore_next = true;
}
self.write_indent();
self.print_css_comment(comment);
Expand All @@ -352,9 +352,9 @@ impl<'a> Printer<'a> {
self.write("\n");
}
self.write_indent();
if prettier_ignore_next {
if format_ignore_next {
self.write(nested_rule.span.extract(self.source));
prettier_ignore_next = false;
format_ignore_next = false;
} else {
self.print_css_rule(nested_rule);
}
Expand All @@ -375,9 +375,9 @@ impl<'a> Printer<'a> {
self.write("\n");
}
self.write_indent();
if prettier_ignore_next {
if format_ignore_next {
self.write(nested_atrule.span.extract(self.source));
prettier_ignore_next = false;
format_ignore_next = false;
} else {
self.print_css_atrule(nested_atrule);
}
Expand Down
8 changes: 4 additions & 4 deletions crates/tsv_css/src/printer/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ use tsv_lang::{
self,
arena::{DocArena, DocId},
},
printing,
is_format_ignore_directive, printing,
};

/// Check if function args have wrappable content (break points)
Expand Down Expand Up @@ -224,10 +224,10 @@ impl<'a> Printer<'a> {
let comments_before =
self.print_leading_comments(prev_end, node_start, &mut comment_idx);

// Check printed comments for prettier-ignore (O(k) where k = comments_before)
// Check printed comments for a format-ignore directive (O(k) where k = comments_before)
let has_ignore = self.comments[idx_before..comment_idx]
.iter()
.any(|c| c.content.trim() == "prettier-ignore");
.any(|c| is_format_ignore_directive(&c.content));

// Add separator before node
if printed_any || comments_before > 0 {
Expand All @@ -247,7 +247,7 @@ impl<'a> Printer<'a> {
}
}

// prettier-ignore: emit raw source instead of formatting
// format-ignore: emit raw source instead of formatting
if has_ignore {
self.write(node.span().extract(self.source));
} else {
Expand Down
21 changes: 11 additions & 10 deletions crates/tsv_css/src/printer/rules.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@

use super::Printer;
use crate::ast::internal;
use tsv_lang::is_format_ignore_directive;

impl<'a> Printer<'a> {
/// Format a CSS rule (selector + declarations block)
Expand Down Expand Up @@ -49,7 +50,7 @@ impl<'a> Printer<'a> {
// Format declarations and comments with indentation
self.indent_level += 1;
let mut i = start_index;
let mut prettier_ignore_next = false;
let mut format_ignore_next = false;
while i < rule.declarations.len() {
let child = &rule.declarations[i];
match child {
Expand All @@ -58,7 +59,7 @@ impl<'a> Printer<'a> {
if i > start_index && self.has_blank_line_before_child(&rule.declarations, i) {
self.write("\n");
}
if prettier_ignore_next {
if format_ignore_next {
// Emit raw source instead of formatting
// Span doesn't include semicolon — add it like write_declaration_end
self.write_indent();
Expand All @@ -67,7 +68,7 @@ impl<'a> Printer<'a> {
self.write(" !important");
}
self.write(";\n");
prettier_ignore_next = false;
format_ignore_next = false;
} else {
self.print_css_declaration(decl);
}
Expand All @@ -89,9 +90,9 @@ impl<'a> Printer<'a> {
self.write("\n");
}

// Check for prettier-ignore
if comment.content.trim() == "prettier-ignore" {
prettier_ignore_next = true;
// Check for a format-ignore directive
if is_format_ignore_directive(&comment.content) {
format_ignore_next = true;
}

self.write_indent();
Expand All @@ -105,9 +106,9 @@ impl<'a> Printer<'a> {
self.write("\n");
}
self.write_indent();
if prettier_ignore_next {
if format_ignore_next {
self.write(nested_rule.span.extract(self.source));
prettier_ignore_next = false;
format_ignore_next = false;
} else {
self.print_css_rule(nested_rule);
}
Expand All @@ -127,9 +128,9 @@ impl<'a> Printer<'a> {
self.write("\n");
}
self.write_indent();
if prettier_ignore_next {
if format_ignore_next {
self.write(nested_atrule.span.extract(self.source));
prettier_ignore_next = false;
format_ignore_next = false;
} else {
self.print_css_atrule(nested_atrule);
}
Expand Down
4 changes: 4 additions & 0 deletions crates/tsv_debug/src/cli/commands/ts_fixture_audit.rs
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,10 @@ const INTENTIONAL_TS: &[(&str, &str)] = &[
"typescript/syntax/unicode_line_terminators",
"pins U+2028/U+2029 line counting on the standalone tsv_ts loc path (LocationTracker::new_ecmascript, acorn's LineTerminator set) — formatting is context-invariant but the .svelte path tracks LF-only locations (Svelte's locate-character), so embedding would pin different locs",
),
(
"typescript/syntax/comments/format_ignore_prettier_divergence",
"format-convertible (the directive works the same embedded), but kept .ts on purpose to pin the standalone tsv_ts + prettier-typescript-parser path for the format-ignore directive — the Svelte-embedded coverage lives in svelte/syntax/format_ignore/",
),
];

/// Look up a fixture in `INTENTIONAL_TS` by relative-path suffix.
Expand Down
4 changes: 4 additions & 0 deletions crates/tsv_lang/CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,10 @@ See [../../CLAUDE.md §Comment Handling](../../CLAUDE.md#comment-handling-detach
| `has_line_comments_in_range()` | Existence check restricted to line comments |
| `has_multiline_block_comments_in_range()` | Existence check for multi-line block comments (force expansion) |

### Directive Recognition

`is_format_ignore_directive()` / `is_format_ignore_range_start()` / `is_format_ignore_range_end()` are the single source of truth for the format-suppression directive set — the tsv-native `format-ignore` family plus prettier's `prettier-ignore` family (drop-in compat). Each operates on trimmed comment text and is called by all three language printers (`tsv_ts`, `tsv_css`, `tsv_svelte`), since the comment types differ across crates. See [docs/directives.md](../../docs/directives.md) and [docs/conformance_prettier.md §Format-ignore directive](../../docs/conformance_prettier.md#format-ignore-directive).

## Interner Traits

String interning deduplicates identifiers across all languages in a file. Symbols flow from parser through doc builder to renderer:
Expand Down
58 changes: 58 additions & 0 deletions crates/tsv_lang/src/comment.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,42 @@ pub struct Comment {
pub emit_character_field: bool,
}

//
// Format-Ignore Directive Recognition
//
//
// A comment can suppress formatting of the construct that follows it. tsv
// recognizes its own tool-neutral `format-ignore` family as canonical and
// prettier's `prettier-ignore` family as a drop-in-compatible alias — both
// spellings are honored everywhere. These predicates are the single source of
// truth for the directive set, called by each language printer (the comment
// types differ across crates, so the shared atom operates on the trimmed text).

/// Whether `content` is a `format-ignore` / `prettier-ignore` directive — emit
/// the following construct as raw source instead of formatting it.
#[inline]
pub fn is_format_ignore_directive(content: &str) -> bool {
matches!(content.trim(), "format-ignore" | "prettier-ignore")
}

/// Whether `content` opens an ignore range (`format-ignore-start` /
/// `prettier-ignore-start`). Everything through the matching range-end marker is
/// emitted as raw source.
#[inline]
pub fn is_format_ignore_range_start(content: &str) -> bool {
matches!(
content.trim(),
"format-ignore-start" | "prettier-ignore-start"
)
}

/// Whether `content` closes an ignore range (`format-ignore-end` /
/// `prettier-ignore-end`). See `is_format_ignore_range_start`.
#[inline]
pub fn is_format_ignore_range_end(content: &str) -> bool {
matches!(content.trim(), "format-ignore-end" | "prettier-ignore-end")
}

//
// Comment Classification
//
Expand Down Expand Up @@ -251,6 +287,28 @@ mod tests {
}
}

#[test]
fn format_ignore_directives_recognize_both_spellings() {
// The tsv-native `format-ignore` family and prettier's `prettier-ignore`
// family are both honored, with surrounding whitespace trimmed (block
// comments arrive as ` format-ignore `).
assert!(is_format_ignore_directive("format-ignore"));
assert!(is_format_ignore_directive("prettier-ignore"));
assert!(is_format_ignore_directive(" format-ignore "));
assert!(!is_format_ignore_directive("format-ignore-start"));
assert!(!is_format_ignore_directive("eslint-disable"));

assert!(is_format_ignore_range_start("format-ignore-start"));
assert!(is_format_ignore_range_start("prettier-ignore-start"));
assert!(!is_format_ignore_range_start("format-ignore"));
assert!(!is_format_ignore_range_start("format-ignore-end"));

assert!(is_format_ignore_range_end("format-ignore-end"));
assert!(is_format_ignore_range_end("prettier-ignore-end"));
assert!(!is_format_ignore_range_end("format-ignore"));
assert!(!is_format_ignore_range_end("format-ignore-start"));
}

#[test]
fn comments_in_range_respects_start_and_end_boundaries() {
let comments = vec![
Expand Down
3 changes: 2 additions & 1 deletion crates/tsv_lang/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,8 @@ mod span;
pub use comment::{
ClassifiedComments, Comment, CommentPosition, classify_comment, classify_comment_fast,
comments_after, comments_in_range, find_first_comment_from, has_comments_in_range,
has_line_comments_in_range, has_multiline_block_comments_in_range,
has_line_comments_in_range, has_multiline_block_comments_in_range, is_format_ignore_directive,
is_format_ignore_range_end, is_format_ignore_range_start,
};
pub use config::{EmbedContext, INDENT, LayoutMode, PRINT_WIDTH, TAB_WIDTH};
pub use error::{ErrorContext, ParseError, Result};
Expand Down
Loading