mirror of
https://github.com/oxc-project/oxc.git
synced 2026-11-04 10:29:58 +00:00
Fixes #28263 - Before this PR: `.html` and also html-in-{js,md,etc} are formatted by Prettier. - Because of that, we had to run JS, which became an overhead. - After this PR: All of them are formatted natively by `oxc_formatter_htmlish`. - No overhead. At the same time, since we are fixing Prettier bugs and inconsistent behavior, please refer to DIVERGENCES.md for the differences.
186 lines
7.2 KiB
Rust
186 lines
7.2 KiB
Rust
//! Fixture tests for cases the Prettier conformance suite does NOT cover,
|
|
//! plus minimal re-pins of known divergences the suite DOES hit
|
|
//! (a divergence needs its own pin; the big conformance file is only a regression net).
|
|
//!
|
|
//! Expected outputs were verified against `prettier` by hand;
|
|
//! when adding a fixture, do the same (`npx prettier@<oxfmt-bundle-version> --parser <variant>`).
|
|
|
|
use std::path::Path;
|
|
|
|
use oxc_allocator::{Allocator, ArenaVec};
|
|
use oxc_formatter_css::{CssFormatOptions, CssRoot, CssVariant, format, parse_for_format};
|
|
use oxc_formatter_tests::{FixtureFormatter, OptionSet, build_fixture_snapshot};
|
|
|
|
mod options;
|
|
use options::apply_css_options;
|
|
|
|
struct CssHarness;
|
|
|
|
/// What formatting must leave unchanged, see `FixtureFormatter::Fingerprint`.
|
|
#[derive(Debug, PartialEq)]
|
|
struct Fingerprint {
|
|
comments: usize,
|
|
}
|
|
|
|
impl FixtureFormatter for CssHarness {
|
|
type Options = CssFormatOptions;
|
|
type Fingerprint = Fingerprint;
|
|
|
|
fn parse_options(json: &OptionSet) -> Self::Options {
|
|
let mut options = CssFormatOptions::default();
|
|
apply_css_options(&mut options, json);
|
|
options
|
|
}
|
|
|
|
fn format(source: &str, path: &Path, options: &Self::Options) -> String {
|
|
let options = CssFormatOptions { variant: variant_of(path), ..*options };
|
|
|
|
if is_embedded(path) {
|
|
return format_embedded(source, options);
|
|
}
|
|
|
|
let allocator = Allocator::default();
|
|
format(&allocator, source, options)
|
|
.expect("format should succeed")
|
|
.print()
|
|
.expect("print should succeed")
|
|
.into_code()
|
|
}
|
|
|
|
fn fingerprint(source: &str, path: &Path, options: &Self::Options) -> Fingerprint {
|
|
let options = CssFormatOptions { variant: variant_of(path), ..*options };
|
|
let allocator = Allocator::default();
|
|
let parsed = parse_for_format(
|
|
&allocator,
|
|
source,
|
|
options,
|
|
if is_embedded(path) { CssRoot::CssInJsTemplate } else { CssRoot::Stylesheet },
|
|
)
|
|
.expect("source should parse");
|
|
Fingerprint { comments: parsed.comments.len() }
|
|
}
|
|
}
|
|
|
|
/// The dialect comes from the fixture extension, like oxfmt's classifier.
|
|
fn variant_of(path: &Path) -> CssVariant {
|
|
match path.extension().and_then(|e| e.to_str()) {
|
|
Some("scss") => CssVariant::Scss,
|
|
Some("less") => CssVariant::Less,
|
|
_ => CssVariant::Css,
|
|
}
|
|
}
|
|
|
|
/// Fixtures under `embedded/` exercise the dispatcher entry point
|
|
/// (`format_to_ir`, css-in-js), which tolerates
|
|
/// `` `PLACEHOLDER-N` `` markers in value/selector position.
|
|
fn is_embedded(path: &Path) -> bool {
|
|
path.components().any(|c| c.as_os_str() == "embedded")
|
|
}
|
|
|
|
/// Format through `format_to_ir` and print the raw IR, mirroring what the
|
|
/// oxfmt dispatcher + parent template printing do (minus `${}` substitution).
|
|
fn format_embedded(source: &str, options: CssFormatOptions) -> String {
|
|
use oxc_formatter_core::{Document, FormatElement, FormatOptions};
|
|
|
|
let allocator = Allocator::default();
|
|
let session =
|
|
oxc_formatter_core::FormatSession::new(&allocator, oxc_formatter_core::InputKind::Fragment);
|
|
let embedded = oxc_formatter_css::format_to_ir(
|
|
&session,
|
|
source,
|
|
options,
|
|
Some(oxc_formatter_css::CssEmbeddedIn::CssInJsTemplate),
|
|
)
|
|
.expect("format should succeed");
|
|
// Simulate the host: replace each typed placeholder with the canonical
|
|
// sentinel (the real host substitutes `${expr}`; tests have no expressions).
|
|
// The printer `debug_assert`s on any surviving `EmbedPlaceholder`.
|
|
let elements = ArenaVec::from_iter_in(
|
|
embedded.ir.iter().map(|element| match element {
|
|
FormatElement::EmbedPlaceholder(index) => {
|
|
let text = allocator.alloc_str(&std::format!("`PLACEHOLDER-{index}`"));
|
|
FormatElement::text(text, options.indent_width)
|
|
}
|
|
other => other.clone(),
|
|
}),
|
|
&session.allocator(),
|
|
);
|
|
let mut code = Document::new(elements, Vec::new())
|
|
.print(source.len(), options.as_print_options())
|
|
.expect("print should succeed")
|
|
.into_code();
|
|
// The embedded entry point emits no trailing newline (the parent owns it);
|
|
// add one so snapshots stay diff-friendly.
|
|
code.push('\n');
|
|
code
|
|
}
|
|
|
|
fn test_file(path: &Path) {
|
|
// `insta::assert_snapshot!` is invoked from this file so the snapshot's
|
|
// `source:` header records this consumer crate, not the shared harness.
|
|
let snap = build_fixture_snapshot::<CssHarness>(path);
|
|
insta::with_settings!({
|
|
snapshot_path => snap.path,
|
|
prepend_module_to_snapshot => false,
|
|
snapshot_suffix => "",
|
|
omit_expression => true,
|
|
}, {
|
|
insta::assert_snapshot!(snap.name, snap.body);
|
|
});
|
|
}
|
|
|
|
// Include auto-generated test functions from build.rs
|
|
include!(concat!(env!("OUT_DIR"), "/generated_tests.rs"));
|
|
|
|
// ---
|
|
|
|
#[test]
|
|
fn bom_is_preserved() {
|
|
let allocator = Allocator::default();
|
|
let formatted =
|
|
format(&allocator, "\u{feff}a {\n color: red;\n}\n", CssFormatOptions::default())
|
|
.expect("BOM input should parse")
|
|
.print()
|
|
.expect("print should succeed")
|
|
.into_code();
|
|
assert_eq!(formatted, "\u{feff}a {\n color: red;\n}\n");
|
|
}
|
|
|
|
#[test]
|
|
fn parse_error_is_err() {
|
|
let allocator = Allocator::default();
|
|
let css = CssFormatOptions::default();
|
|
let scss = CssFormatOptions { variant: CssVariant::Scss, ..css };
|
|
let less = CssFormatOptions { variant: CssVariant::Less, ..css };
|
|
for (source, options) in [
|
|
// Root declaration: `TopLevelDeclaration` in a stylesheet, as opposed to a fragment (`CssRoot`)
|
|
// (dart-sass rejects it; Less parses it like less.js).
|
|
("display: flex;", scss),
|
|
// EOF/newline-unclosed constructs: oxc-css-parser (0.0.6+) recovers to a
|
|
// valid AST but records the spec parse error, so they bail like every
|
|
// other recoverable error. Prettier rejects all of these too ("Unclosed
|
|
// block" / "Unclosed string" / "Unclosed bracket"); formatting them
|
|
// would corrupt the input (`;` appended inside the unclosed construct).
|
|
("a {\n color: red;\n", css),
|
|
("@media (min-width: 500px) {\n", css),
|
|
("a { content: \"abc", css),
|
|
("a {\n content: \"\n}", css),
|
|
("a { width: calc(100% - 10px", css),
|
|
("a { --x: {", css),
|
|
// css-in-js `${}` markers in value position...
|
|
("a { color: `PLACEHOLDER-0`; }", scss),
|
|
// ...and in selector position stay errors in the STANDALONE entry
|
|
// (`format_to_ir` tolerates them via the oxc-css-parser fork option;
|
|
// see `fixtures/embedded/`).
|
|
(".a-`PLACEHOLDER-0` {\n}", scss),
|
|
// The raw-prelude fallback (`unknown-qualified-rule.css`) fires only
|
|
// when a declaration's value hits a top-level `{` (name + `:` parsed);
|
|
// a selector error with no such head stays a hard error.
|
|
("a, { color: red; }", css),
|
|
// Pin for crates/oxc_formatter_css/DIVERGENCES.md#less-value-interpolation-rejected
|
|
(".a { width: @{min-width}; }", less),
|
|
] {
|
|
assert!(format(&allocator, source, options).is_err(), "{source:?} should fail to format");
|
|
}
|
|
}
|