Skip to content

Commit

Permalink
feat(prettier): Complete print_literal (#7952)
Browse files Browse the repository at this point in the history
Part of #5068 

- [x] RegExpLiteral
- [x] BigIntLiteral
- [x] NumericLiteral
- [x] StringLiteral
  - [x] replaceEndOfLine
  - [x] makeString keep usless escape
- [x] NullLiteral
- [x] BooleanLiteral
  • Loading branch information
leaysgur authored Dec 17, 2024
1 parent de8a86e commit a4e8ce8
Show file tree
Hide file tree
Showing 7 changed files with 227 additions and 174 deletions.
131 changes: 21 additions & 110 deletions crates/oxc_prettier/src/format/js.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
},
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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))
}
}

Expand All @@ -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,
)
}
}

Expand Down Expand Up @@ -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)
Expand All @@ -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)
Expand Down Expand Up @@ -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::<String>();
dynamic_text!(p, &sorted)
}
}
183 changes: 183 additions & 0 deletions crates/oxc_prettier/src/format/print/literal.rs
Original file line number Diff line number Diff line change
@@ -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 = &quoted_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::<Vec<_>>();
join!(p, replacement, lines)
}
2 changes: 1 addition & 1 deletion crates/oxc_prettier/src/format/print/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Loading

0 comments on commit a4e8ce8

Please sign in to comment.