From a4e8ce8fe5ef696a1882d7c42aed8e1190eb1ccc Mon Sep 17 00:00:00 2001 From: Yuji Sugiura <6259812+leaysgur@users.noreply.github.com> Date: Tue, 17 Dec 2024 13:02:22 +0900 Subject: [PATCH] feat(prettier): Complete `print_literal` (#7952) Part of #5068 - [x] RegExpLiteral - [x] BigIntLiteral - [x] NumericLiteral - [x] StringLiteral - [x] replaceEndOfLine - [x] makeString keep usless escape - [x] NullLiteral - [x] BooleanLiteral --- crates/oxc_prettier/src/format/js.rs | 131 ++----------- .../oxc_prettier/src/format/print/literal.rs | 183 ++++++++++++++++++ crates/oxc_prettier/src/format/print/mod.rs | 2 +- .../oxc_prettier/src/format/print/string.rs | 61 ------ crates/oxc_prettier/src/ir/doc.rs | 1 + crates/oxc_prettier/src/macros.rs | 20 ++ .../snapshots/prettier.js.snap.md | 3 +- 7 files changed, 227 insertions(+), 174 deletions(-) create mode 100644 crates/oxc_prettier/src/format/print/literal.rs delete mode 100644 crates/oxc_prettier/src/format/print/string.rs diff --git a/crates/oxc_prettier/src/format/js.rs b/crates/oxc_prettier/src/format/js.rs index c64ea002a27d7..ab20031f348dc 100644 --- a/crates/oxc_prettier/src/format/js.rs +++ b/crates/oxc_prettier/src/format/js.rs @@ -11,7 +11,8 @@ use crate::{ format::{ print::{ array, arrow_function, assignment, binaryish, block, call_expression, class, function, - function_parameters, misc, module, object, property, string, template_literal, ternary, + function_parameters, literal, misc, module, object, property, template_literal, + ternary, }, Format, }, @@ -59,9 +60,10 @@ impl<'a> Format<'a> for Hashbang<'a> { impl<'a> Format<'a> for Directive<'a> { fn format(&self, p: &mut Prettier<'a>) -> Doc<'a> { let mut parts = Vec::new_in(p.allocator); - parts.push(dynamic_text!( + parts.push(literal::print_string_from_not_quoted_raw_text( p, - string::print_string(p, self.directive.as_str(), p.options.single_quote,) + self.directive.as_str(), + p.options.single_quote, )); if let Some(semi) = p.semi() { parts.push(semi); @@ -905,64 +907,7 @@ impl<'a> Format<'a> for NullLiteral { impl<'a> Format<'a> for NumericLiteral<'a> { fn format(&self, p: &mut Prettier<'a>) -> Doc<'a> { - wrap!(p, self, NumericLiteral, { - // See https://github.com/prettier/prettier/blob/3.3.3/src/utils/print-number.js - // Perf: the regexes from prettier code above are ported to manual search for performance reasons. - let mut string = self.span.source_text(p.source_text).cow_to_ascii_lowercase(); - - // Remove unnecessary plus and zeroes from scientific notation. - if let Some((head, tail)) = string.split_once('e') { - let negative = if tail.starts_with('-') { "-" } else { "" }; - let trimmed = tail.trim_start_matches(['+', '-']).trim_start_matches('0'); - if trimmed.starts_with(|c: char| c.is_ascii_digit()) { - string = Cow::Owned(std::format!("{head}e{negative}{trimmed}")); - } - } - - // Remove unnecessary scientific notation (1e0). - if let Some((head, tail)) = string.split_once('e') { - if tail.trim_start_matches(['+', '-']).trim_start_matches('0').is_empty() { - string = Cow::Owned(head.to_string()); - } - } - - // Make sure numbers always start with a digit. - if string.starts_with('.') { - string = Cow::Owned(std::format!("0{string}")); - } - - // Remove extraneous trailing decimal zeroes. - if let Some((head, tail)) = string.split_once('.') { - if let Some((head_e, tail_e)) = tail.split_once('e') { - if !head_e.is_empty() { - let trimmed = head_e.trim_end_matches('0'); - if trimmed.is_empty() { - string = Cow::Owned(std::format!("{head}.0e{tail_e}")); - } else { - string = Cow::Owned(std::format!("{head}.{trimmed}e{tail_e}")); - } - } - } else if !tail.is_empty() { - let trimmed = tail.trim_end_matches('0'); - if trimmed.is_empty() { - string = Cow::Owned(std::format!("{head}.0")); - } else { - string = Cow::Owned(std::format!("{head}.{trimmed}")); - } - } - } - - // Remove trailing dot. - if let Some((head, tail)) = string.split_once('.') { - if tail.is_empty() { - string = Cow::Owned(head.to_string()); - } else if tail.starts_with('e') { - string = Cow::Owned(std::format!("{head}{tail}")); - } - } - - dynamic_text!(p, &string) - }) + literal::print_number(p, self.span.source_text(p.source_text)) } } @@ -977,22 +922,17 @@ impl<'a> Format<'a> for BigIntLiteral<'a> { impl<'a> Format<'a> for RegExpLiteral<'a> { fn format(&self, p: &mut Prettier<'a>) -> Doc<'a> { - let mut parts = Vec::new_in(p.allocator); - parts.push(text!("/")); - parts.push(dynamic_text!(p, self.regex.pattern.source_text(p.source_text).as_ref())); - parts.push(text!("/")); - parts.push(self.regex.flags.format(p)); - array!(p, parts) + dynamic_text!(p, &self.regex.to_string()) } } impl<'a> Format<'a> for StringLiteral<'a> { fn format(&self, p: &mut Prettier<'a>) -> Doc<'a> { - wrap!(p, self, StringLiteral, { - let raw = &p.source_text[(self.span.start + 1) as usize..(self.span.end - 1) as usize]; - // TODO: implement `makeString` from prettier/src/utils/print-string.js - dynamic_text!(p, string::print_string(p, raw, p.options.single_quote)) - }) + literal::replace_end_of_line( + p, + literal::print_string(p, self.span.source_text(p.source_text), p.options.single_quote), + JoinSeparator::Literalline, + ) } } @@ -1214,9 +1154,10 @@ impl<'a> Format<'a> for PropertyKey<'a> { match self { PropertyKey::StaticIdentifier(ident) => { if need_quote { - dynamic_text!( + literal::print_string_from_not_quoted_raw_text( p, - string::print_string(p, &ident.name, p.options.single_quote) + &ident.name, + p.options.single_quote, ) } else { ident.format(p) @@ -1233,17 +1174,19 @@ impl<'a> Format<'a> for PropertyKey<'a> { { dynamic_text!(p, literal.value.as_str()) } else { - dynamic_text!( + literal::print_string_from_not_quoted_raw_text( p, - string::print_string(p, literal.value.as_str(), p.options.single_quote,) + literal.value.as_str(), + p.options.single_quote, ) } } PropertyKey::NumericLiteral(literal) => { if need_quote { - dynamic_text!( + literal::print_string_from_not_quoted_raw_text( p, - string::print_string(p, &literal.raw_str(), p.options.single_quote) + &literal.raw_str(), + p.options.single_quote, ) } else { literal.format(p) @@ -1723,35 +1666,3 @@ impl<'a> Format<'a> for AssignmentPattern<'a> { }) } } - -impl<'a> Format<'a> for RegExpFlags { - fn format(&self, p: &mut Prettier<'a>) -> Doc<'a> { - let mut string = std::vec::Vec::with_capacity(self.iter().count()); - if self.contains(Self::D) { - string.push('d'); - } - if self.contains(Self::G) { - string.push('g'); - } - if self.contains(Self::I) { - string.push('i'); - } - if self.contains(Self::M) { - string.push('m'); - } - if self.contains(Self::S) { - string.push('s'); - } - if self.contains(Self::U) { - string.push('u'); - } - if self.contains(Self::V) { - string.push('v'); - } - if self.contains(Self::Y) { - string.push('y'); - } - let sorted = string.iter().collect::(); - dynamic_text!(p, &sorted) - } -} diff --git a/crates/oxc_prettier/src/format/print/literal.rs b/crates/oxc_prettier/src/format/print/literal.rs new file mode 100644 index 0000000000000..aa1e56e512617 --- /dev/null +++ b/crates/oxc_prettier/src/format/print/literal.rs @@ -0,0 +1,183 @@ +use std::borrow::Cow; + +use cow_utils::CowUtils; +use oxc_allocator::String; +use oxc_span::Span; + +use crate::{ + dynamic_text, + ir::{Doc, JoinSeparator}, + join, Prettier, +}; + +/// Print quoted string. +/// Quotes are automatically chosen based on the content of the string and option. +pub fn print_string<'a>( + p: &Prettier<'a>, + quoted_raw_text: &'a str, + prefer_single_quote: bool, +) -> Doc<'a> { + debug_assert!( + quoted_raw_text.starts_with('\'') && quoted_raw_text.ends_with('\'') + || quoted_raw_text.starts_with('"') && quoted_raw_text.ends_with('"') + ); + + let original_quote = quoted_raw_text.chars().next().unwrap(); + let not_quoted_raw_text = "ed_raw_text[1..quoted_raw_text.len() - 1]; + + let enclosing_quote = get_preferred_quote(not_quoted_raw_text, prefer_single_quote); + + // This keeps useless escape as-is + if original_quote == enclosing_quote { + return dynamic_text!(p, quoted_raw_text); + } + + dynamic_text!(p, make_string(p, not_quoted_raw_text, enclosing_quote).into_bump_str()) +} + +// TODO: Can this be removed? It does not exist in Prettier +/// Print quoted string from not quoted text. +/// Mainly this is used to add quotes for object property keys. +pub fn print_string_from_not_quoted_raw_text<'a>( + p: &Prettier<'a>, + not_quoted_raw_text: &str, + prefer_single_quote: bool, +) -> Doc<'a> { + let enclosing_quote = get_preferred_quote(not_quoted_raw_text, prefer_single_quote); + dynamic_text!(p, make_string(p, not_quoted_raw_text, enclosing_quote).into_bump_str()) +} + +// See https://github.com/prettier/prettier/blob/3.3.3/src/utils/print-number.js +// Perf: the regexes from prettier code above are ported to manual search for performance reasons. +pub fn print_number<'a>(p: &Prettier<'a>, raw_text: &str) -> Doc<'a> { + let mut string = raw_text.cow_to_ascii_lowercase(); + + // Remove unnecessary plus and zeroes from scientific notation. + if let Some((head, tail)) = string.split_once('e') { + let negative = if tail.starts_with('-') { "-" } else { "" }; + let trimmed = tail.trim_start_matches(['+', '-']).trim_start_matches('0'); + if trimmed.starts_with(|c: char| c.is_ascii_digit()) { + string = Cow::Owned(std::format!("{head}e{negative}{trimmed}")); + } + } + + // Remove unnecessary scientific notation (1e0). + if let Some((head, tail)) = string.split_once('e') { + if tail.trim_start_matches(['+', '-']).trim_start_matches('0').is_empty() { + string = Cow::Owned(head.to_string()); + } + } + + // Make sure numbers always start with a digit. + if string.starts_with('.') { + string = Cow::Owned(std::format!("0{string}")); + } + + // Remove extraneous trailing decimal zeroes. + if let Some((head, tail)) = string.split_once('.') { + if let Some((head_e, tail_e)) = tail.split_once('e') { + if !head_e.is_empty() { + let trimmed = head_e.trim_end_matches('0'); + if trimmed.is_empty() { + string = Cow::Owned(std::format!("{head}.0e{tail_e}")); + } else { + string = Cow::Owned(std::format!("{head}.{trimmed}e{tail_e}")); + } + } + } else if !tail.is_empty() { + let trimmed = tail.trim_end_matches('0'); + if trimmed.is_empty() { + string = Cow::Owned(std::format!("{head}.0")); + } else { + string = Cow::Owned(std::format!("{head}.{trimmed}")); + } + } + } + + // Remove trailing dot. + if let Some((head, tail)) = string.split_once('.') { + if tail.is_empty() { + string = Cow::Owned(head.to_string()); + } else if tail.starts_with('e') { + string = Cow::Owned(std::format!("{head}{tail}")); + } + } + + dynamic_text!(p, &string) +} + +pub fn get_preferred_quote(not_quoted_raw_text: &str, prefer_single_quote: bool) -> char { + let (preferred_quote_char, alternate_quote_char) = + if prefer_single_quote { ('\'', '"') } else { ('"', '\'') }; + + let mut preferred_quote_count = 0; + let mut alternate_quote_count = 0; + + for character in not_quoted_raw_text.chars() { + if character == preferred_quote_char { + preferred_quote_count += 1; + } else if character == alternate_quote_char { + alternate_quote_count += 1; + } + } + + if preferred_quote_count > alternate_quote_count { + alternate_quote_char + } else { + preferred_quote_char + } +} + +fn make_string<'a>( + p: &Prettier<'a>, + not_quoted_raw_text: &str, + enclosing_quote: char, +) -> String<'a> { + let other_quote = if enclosing_quote == '"' { '\'' } else { '"' }; + + let mut result = String::new_in(p.allocator); + result.push(enclosing_quote); + + let mut chars = not_quoted_raw_text.chars().peekable(); + while let Some(c) = chars.next() { + if c == '\\' { + if let Some(&nc) = chars.peek() { + if nc == other_quote { + // Skip(remove) useless escape + chars.next(); + result.push(nc); + } else { + result.push('\\'); + if let Some(nc) = chars.next() { + result.push(nc); + } + } + } else { + result.push('\\'); + } + } else if c == enclosing_quote { + result.push('\\'); + result.push(c); + } else { + result.push(c); + } + } + + result.push(enclosing_quote); + result +} + +/// Handle line continuation. +/// This does not recursively handle the doc, expects single `Doc::Str`. +pub fn replace_end_of_line<'a>( + p: &Prettier<'a>, + doc: Doc<'a>, + replacement: JoinSeparator, +) -> Doc<'a> { + let Doc::Str(text) = doc else { + return doc; + }; + + let lines = text.split('\n').map(|line| dynamic_text!(p, line)).collect::>(); + join!(p, replacement, lines) +} diff --git a/crates/oxc_prettier/src/format/print/mod.rs b/crates/oxc_prettier/src/format/print/mod.rs index 411902c3e0b46..8870118a2aba8 100644 --- a/crates/oxc_prettier/src/format/print/mod.rs +++ b/crates/oxc_prettier/src/format/print/mod.rs @@ -8,11 +8,11 @@ pub mod call_expression; pub mod class; pub mod function; pub mod function_parameters; +pub mod literal; pub mod misc; pub mod module; pub mod object; pub mod property; pub mod statement; -pub mod string; pub mod template_literal; pub mod ternary; diff --git a/crates/oxc_prettier/src/format/print/string.rs b/crates/oxc_prettier/src/format/print/string.rs deleted file mode 100644 index c48ba8ff36279..0000000000000 --- a/crates/oxc_prettier/src/format/print/string.rs +++ /dev/null @@ -1,61 +0,0 @@ -use oxc_allocator::String; - -use crate::Prettier; - -fn get_preferred_quote(raw: &str, prefer_single_quote: bool) -> char { - let (preferred_quote_char, alternate_quote_char) = - if prefer_single_quote { ('\'', '"') } else { ('"', '\'') }; - - let mut preferred_quote_count = 0; - let mut alternate_quote_count = 0; - - for character in raw.chars() { - if character == preferred_quote_char { - preferred_quote_count += 1; - } else if character == alternate_quote_char { - alternate_quote_count += 1; - } - } - - if preferred_quote_count > alternate_quote_count { - alternate_quote_char - } else { - preferred_quote_char - } -} - -fn make_string<'a>(p: &Prettier<'a>, raw_text: &str, enclosing_quote: char) -> String<'a> { - let other_quote = if enclosing_quote == '"' { '\'' } else { '"' }; - let mut result = String::new_in(p.allocator); - result.push(enclosing_quote); - - let mut chars = raw_text.chars().peekable(); - while let Some(c) = chars.next() { - match c { - '\\' => { - if let Some(&next_char) = chars.peek() { - if next_char != other_quote { - result.push('\\'); - } - result.push(next_char); - chars.next(); - } else { - result.push('\\'); - } - } - _ if c == enclosing_quote => { - result.push('\\'); - result.push(c); - } - _ => result.push(c), - } - } - - result.push(enclosing_quote); - result -} - -pub fn print_string<'a>(p: &Prettier<'a>, raw_text: &str, prefer_single_quote: bool) -> &'a str { - let enclosing_quote = get_preferred_quote(raw_text, prefer_single_quote); - make_string(p, raw_text, enclosing_quote).into_bump_str() -} diff --git a/crates/oxc_prettier/src/ir/doc.rs b/crates/oxc_prettier/src/ir/doc.rs index 6338e8ae938e3..c6998b8dea7e9 100644 --- a/crates/oxc_prettier/src/ir/doc.rs +++ b/crates/oxc_prettier/src/ir/doc.rs @@ -87,4 +87,5 @@ pub enum JoinSeparator { Softline, Hardline, CommaLine, // [",", line] + Literalline, } diff --git a/crates/oxc_prettier/src/macros.rs b/crates/oxc_prettier/src/macros.rs index ced992902c83d..8cedddc5d679f 100644 --- a/crates/oxc_prettier/src/macros.rs +++ b/crates/oxc_prettier/src/macros.rs @@ -210,6 +210,7 @@ macro_rules! join { $crate::ir::JoinSeparator::CommaLine => { parts.extend([$crate::text!(","), $crate::line!()]); } + $crate::ir::JoinSeparator::Literalline => parts.extend($crate::literalline!()), } } parts.push(doc); @@ -258,6 +259,25 @@ macro_rules! hardline { }}; } +/// Specify a line break that is always included in the output and doesn't indent the next line. +/// Also, unlike hardline, this kind of line break preserves trailing whitespace on the line it ends. +/// This is used for template literals. +/// +/// ``` +/// literalline!(); +/// ``` +#[macro_export] +macro_rules! literalline { + () => {{ + let literalline = $crate::ir::Doc::Line($crate::ir::Line { + hard: true, + literal: true, + ..Default::default() + }); + [literalline, $crate::ir::Doc::BreakParent] + }}; +} + /// Increase the level of indentation. /// /// ``` diff --git a/tasks/prettier_conformance/snapshots/prettier.js.snap.md b/tasks/prettier_conformance/snapshots/prettier.js.snap.md index f4923de8023b3..677a139bea498 100644 --- a/tasks/prettier_conformance/snapshots/prettier.js.snap.md +++ b/tasks/prettier_conformance/snapshots/prettier.js.snap.md @@ -1,4 +1,4 @@ -js compatibility: 241/641 (37.60%) +js compatibility: 242/641 (37.75%) # Failed @@ -442,7 +442,6 @@ js compatibility: 241/641 (37.60%) * js/strings/escaped.js * js/strings/multiline-literal.js * js/strings/non-octal-eight-and-nine.js -* js/strings/strings.js * js/strings/template-literals.js ### js/switch