use crate::ast::{self, LiteralValue};
use crate::printer::Printer;
use smallvec::{SmallVec, smallvec};
use std::borrow::Cow;
use tsv_lang::Span;
use tsv_lang::doc::DocBuf;
use tsv_lang::doc::arena::DocId;
use tsv_lang::printing::{format_string_literal, is_width_relevant, optimal_string_quote_in};
fn string_literal_prints_verbatim(literal: &ast::Literal<'_>, source: &str) -> Option<bool> {
debug_assert!(
matches!(&literal.value, LiteralValue::String(_)),
"string_literal_prints_verbatim called on non-string literal"
);
let (optimal, plain) = optimal_string_quote_in(
source,
literal.span.start as usize + 1,
literal.span.end as usize - 1,
);
(optimal == literal.string_quote(source) as char).then_some(plain)
}
fn rebuild_string_literal(literal: &ast::Literal<'_>, source: &str) -> String {
let raw_literal = literal.span.extract(source);
format_string_literal(
&raw_literal[1..raw_literal.len() - 1],
literal.string_quote(source) as char,
)
}
pub(crate) fn format_string_literal_from_ast<'s>(
literal: &ast::Literal<'_>,
source: &'s str,
) -> Cow<'s, str> {
match string_literal_prints_verbatim(literal, source) {
Some(_) => Cow::Borrowed(literal.span.extract(source)),
None => Cow::Owned(rebuild_string_literal(literal, source)),
}
}
pub(crate) fn format_directive(raw: &str) -> String {
let content = &raw[1..raw.len() - 1];
if content.contains('\'') || content.contains('"') {
raw.to_string()
} else {
format!("'{content}'")
}
}
pub(crate) fn normalize_number_literal(raw: &str) -> Cow<'_, str> {
if raw.len() == 1 {
return Cow::Borrowed(raw);
}
if raw.ends_with('n') {
return lower_ascii_cow(raw);
}
print_number(raw)
}
fn lower_ascii_cow(s: &str) -> Cow<'_, str> {
if s.bytes().any(|b| b.is_ascii_uppercase()) {
Cow::Owned(s.to_ascii_lowercase())
} else {
Cow::Borrowed(s)
}
}
fn print_number(raw: &str) -> Cow<'_, str> {
let s = lower_ascii_cow(raw);
let s = strip_exponent_plus_and_zeros(s);
let s = strip_zero_exponent(s);
let s = ensure_leading_digit(s);
let s = strip_trailing_fraction_zeros(s);
strip_trailing_dot(s)
}
fn strip_exponent_plus_and_zeros(s: Cow<'_, str>) -> Cow<'_, str> {
let bytes = s.as_bytes();
let mut i = 0;
if matches!(bytes.first(), Some(b'+' | b'-')) {
i += 1;
}
let mantissa_start = i;
while i < bytes.len() && (bytes[i].is_ascii_digit() || bytes[i] == b'.') {
i += 1;
}
if i == mantissa_start || i >= bytes.len() || bytes[i] != b'e' {
return s;
}
i += 1; let after_e = i; let mut sign = "";
match bytes.get(i) {
Some(b'+') => i += 1,
Some(b'-') => {
sign = "-";
i += 1;
}
_ => {}
}
while i < bytes.len() && bytes[i] == b'0' {
i += 1;
}
if i >= bytes.len() || !bytes[i].is_ascii_digit() {
return s;
}
Cow::Owned(format!("{}{}{}", &s[..after_e], sign, &s[i..]))
}
fn strip_zero_exponent(s: Cow<'_, str>) -> Cow<'_, str> {
let Some(e_idx) = s.as_bytes().iter().position(|&b| b == b'e') else {
return s;
};
let mantissa = &s[..e_idx];
let exp = &s[e_idx + 1..];
if mantissa.is_empty() {
return s;
}
let m = mantissa.strip_prefix(['+', '-']).unwrap_or(mantissa);
if m.is_empty() || !m.bytes().all(|b| b.is_ascii_digit() || b == b'.') {
return s;
}
let e = exp.strip_prefix(['+', '-']).unwrap_or(exp);
if !e.is_empty() && e.bytes().all(|b| b == b'0') {
Cow::Owned(mantissa.to_string())
} else {
s
}
}
fn ensure_leading_digit(s: Cow<'_, str>) -> Cow<'_, str> {
if let Some(rest) = s.strip_prefix('.') {
Cow::Owned(format!("0.{rest}"))
} else if let Some(rest) = s.strip_prefix("+.") {
Cow::Owned(format!("+0.{rest}"))
} else if let Some(rest) = s.strip_prefix("-.") {
Cow::Owned(format!("-0.{rest}"))
} else {
s
}
}
fn strip_trailing_fraction_zeros(s: Cow<'_, str>) -> Cow<'_, str> {
let Some(dot) = s.as_bytes().iter().position(|&b| b == b'.') else {
return s;
};
let bytes = s.as_bytes();
let mut i = dot + 1;
if i >= bytes.len() || !bytes[i].is_ascii_digit() {
return s;
}
let frac_start = i;
while i < bytes.len() && bytes[i].is_ascii_digit() {
i += 1;
}
let frac_end = i; if frac_end != bytes.len() && bytes[frac_end] != b'e' {
return s;
}
let mut keep = frac_end;
while keep > frac_start + 1 && bytes[keep - 1] == b'0' {
keep -= 1;
}
if keep == frac_end {
return s;
}
Cow::Owned(format!("{}{}", &s[..keep], &s[frac_end..]))
}
fn strip_trailing_dot(s: Cow<'_, str>) -> Cow<'_, str> {
let bytes = s.as_bytes();
if let Some(dot) = bytes.iter().position(|&b| b == b'.') {
let after = dot + 1;
if after == bytes.len() || bytes[after] == b'e' {
return Cow::Owned(format!("{}{}", &s[..dot], &s[after..]));
}
}
s
}
pub(crate) fn sort_regex_flags(flags: &str) -> String {
let mut chars: SmallVec<[char; 8]> = flags.chars().collect();
chars.sort_unstable();
chars.into_iter().collect()
}
impl<'a> Printer<'a> {
pub(in crate::printer) fn build_literal_doc(&self, literal: &ast::Literal<'_>) -> DocId {
let d = self.d();
match &literal.value {
LiteralValue::Number(_) | LiteralValue::BigInt => {
self.build_number_literal_doc(literal)
}
LiteralValue::String(_) => self.build_string_literal_doc(literal),
LiteralValue::Boolean(b) => d.text(if *b { "true" } else { "false" }),
LiteralValue::Null => d.text("null"),
}
}
pub(in crate::printer) fn build_number_literal_doc(&self, lit: &ast::Literal<'_>) -> DocId {
let raw = lit.span.extract(self.source);
match normalize_number_literal(raw) {
Cow::Borrowed(_) => self.verbatim_literal_doc(lit.span, true),
Cow::Owned(s) => self.d().text_pooled(&s),
}
}
fn verbatim_literal_doc(&self, span: Span, plain: bool) -> DocId {
debug_assert_eq!(
plain,
!span.extract(self.source).bytes().any(is_width_relevant),
"a verbatim literal's `plain` claim disagrees with its own bytes"
);
let d = self.d();
if plain {
d.source_span_plain(span)
} else {
d.source_span(span, self.source)
}
}
pub(in crate::printer) fn build_string_literal_doc(&self, lit: &ast::Literal<'_>) -> DocId {
match string_literal_prints_verbatim(lit, self.source) {
Some(plain) => self.verbatim_literal_doc(lit.span, plain),
None => self
.d()
.text_pooled(&rebuild_string_literal(lit, self.source)),
}
}
pub(super) fn build_private_identifier_doc(
&self,
pid: &ast::PrivateIdentifier<'_>,
span: Span,
) -> DocId {
let d = self.d();
d.concat(&[
d.text("#"),
self.ident_name_doc(pid.name, pid.name_span(span).start),
])
}
pub(in crate::printer) fn build_identifier_doc(&self, id: &ast::Identifier<'_>) -> DocId {
self.build_identifier_doc_inner(id, false, false)
}
pub(in crate::printer) fn build_identifier_doc_with_wrapping_type(
&self,
id: &ast::Identifier<'_>,
) -> DocId {
self.build_identifier_doc_inner(id, true, false)
}
pub(in crate::printer) fn build_identifier_doc_no_decorators(
&self,
id: &ast::Identifier<'_>,
) -> DocId {
self.build_identifier_doc_inner(id, false, true)
}
fn build_identifier_doc_inner(
&self,
id: &ast::Identifier<'_>,
wrap_type_args: bool,
skip_decorators: bool,
) -> DocId {
let d = self.d();
let render_decorators = !skip_decorators && id.decorators().is_some();
if !render_decorators && !id.optional && id.type_annotation().is_none() {
return self.identifier_name_doc(id);
}
let mut parts = DocBuf::new();
let after_modifier = self.push_identifier_head_doc(&mut parts, id);
if let Some(type_annotation) = id.type_annotation() {
parts.push(self.build_binding_type_annotation_doc(
after_modifier,
type_annotation,
wrap_type_args,
));
}
let inner = d.concat(&parts);
if render_decorators && let Some(decorators) = id.decorators() {
self.with_param_decorators(Some(decorators), inner, id.span.start)
} else {
inner
}
}
pub(super) fn build_regex_doc(&self, regex: &ast::RegexLiteral) -> DocId {
let mut w = self.d().pool_writer();
w.push('/');
w.push_str(regex.pattern(self.source));
w.push('/');
w.push_str(&sort_regex_flags(regex.flags(self.source)));
w.finish_text()
}
pub(in crate::printer) fn build_spread_doc(&self, spread: &ast::SpreadElement<'_>) -> DocId {
let d = self.d();
let needs_parens = self.needs_parens(spread.argument, super::ParenContext::SpreadArgument);
let dots_end = spread.span.start + "...".len() as u32;
let arg_start = spread.argument.span().start;
let argument_end = spread.argument.span().end;
let has_trailing_comments =
self.has_comments_to_emit_between(argument_end, spread.span.end);
if let Some(frozen) = self.value_head_frozen_span(dots_end, spread.argument.span()) {
let mut inner: DocBuf = DocBuf::new();
if needs_parens {
inner.push(d.text("("));
}
inner.push(self.build_frozen_expression_doc(spread.argument, frozen));
if has_trailing_comments {
self.append_paren_interior_trailing_comments(&mut inner, spread.paren_interior());
}
if needs_parens {
inner.push(d.text(")"));
}
let value = d.concat(&inner);
return d.concat(&[
d.text("..."),
self.build_continuation_indent(dots_end, arg_start, value),
]);
}
let claim = needs_parens
.then(|| self.left_spine_run_claim(spread.span, dots_end, spread.argument, || true))
.flatten();
let gap_end = claim.map_or(arg_start, |claim| claim.region.end.max(arg_start));
self.with_left_spine_claim(claim, || {
let arg_doc = self.build_expression_doc_with_indent_on_break(spread.argument);
if !needs_parens
&& let Some((run, arg_doc)) =
self.breaking_value_leading_run(dots_end, arg_start, || arg_doc)
{
let mut parts = smallvec![d.text("...")];
self.push_leading_run_before_breaking_value(&mut parts, &run, arg_start);
parts.push(arg_doc);
if has_trailing_comments {
self.append_paren_interior_trailing_comments(
&mut parts,
spread.paren_interior(),
);
}
return d.concat(&parts);
}
let comment_doc = self.build_rhs_comments_glued_opt(dots_end, gap_end);
let mut parts = smallvec![d.text("...")];
if let Some(c) = comment_doc {
parts.push(c);
}
if needs_parens {
parts.push(d.text("("));
}
parts.push(arg_doc);
if has_trailing_comments {
self.append_paren_interior_trailing_comments(&mut parts, spread.paren_interior());
}
if needs_parens {
parts.push(d.text(")"));
}
d.concat(&parts)
})
}
}
pub(in crate::printer) fn is_valid_js_identifier(s: &str) -> bool {
use unicode_ident::{is_xid_continue, is_xid_start};
let mut chars = s.chars();
match chars.next() {
Some(c) if is_xid_start(c) || c == '_' || c == '$' => {}
_ => return false,
}
chars.all(|c| is_xid_continue(c) || c == '$')
}
pub(in crate::printer) fn is_factory_identifier_name(name: &str) -> bool {
name.chars().next().is_some_and(char::is_uppercase)
|| (!name.is_empty() && name.chars().all(|c| c == '$' || c == '_'))
}
#[cfg(test)]
mod tests {
use super::format_directive as fd;
use super::normalize_number_literal as norm;
#[test]
fn directive_swaps_outer_quote_to_single() {
assert_eq!(fd("\"use strict\""), "'use strict'");
assert_eq!(fd("\"use asm\""), "'use asm'");
assert_eq!(fd("'use strict'"), "'use strict'");
assert_eq!(fd("\"a\\nb\""), "'a\\nb'");
}
#[test]
fn directive_kept_verbatim_when_content_has_a_quote() {
assert_eq!(fd("\"\\\"\""), "\"\\\"\""); assert_eq!(fd("'\\''"), "'\\''"); assert_eq!(fd("'a\"b'"), "'a\"b'"); }
#[test]
fn keeps_single_char_and_simple() {
assert_eq!(norm("0"), "0");
assert_eq!(norm("5"), "5");
assert_eq!(norm("123"), "123");
assert_eq!(norm("1.0"), "1.0"); }
#[test]
fn exponent_plus_and_leading_zeros() {
assert_eq!(norm("2E+10"), "2e10");
assert_eq!(norm("1e+1"), "1e1");
assert_eq!(norm("1.1e0010"), "1.1e10");
assert_eq!(norm("1e-05"), "1e-5"); assert_eq!(norm(".1e+0010"), "0.1e10");
}
#[test]
fn zero_exponent_dropped() {
assert_eq!(norm("0.5e0"), "0.5");
assert_eq!(norm("1e0"), "1");
assert_eq!(norm("1e-0"), "1");
}
#[test]
fn leading_and_trailing_dot() {
assert_eq!(norm(".5"), "0.5");
assert_eq!(norm("-.5"), "-0.5");
assert_eq!(norm("5."), "5");
assert_eq!(norm("1.e1"), "1e1");
}
#[test]
fn trailing_fraction_zeros() {
assert_eq!(norm("1.00500"), "1.005");
assert_eq!(norm("1.50"), "1.5");
assert_eq!(norm("0.0000"), "0.0");
assert_eq!(norm("500600.001230045000"), "500600.001230045");
}
#[test]
fn radix_literals_only_lowercased() {
assert_eq!(norm("0xFF"), "0xff");
assert_eq!(norm("0xE5"), "0xe5"); assert_eq!(norm("0o17"), "0o17");
assert_eq!(norm("0B101"), "0b101");
}
#[test]
fn bigint_keeps_suffix() {
assert_eq!(norm("100n"), "100n");
assert_eq!(norm("0xFFn"), "0xffn");
assert_eq!(norm("0x1Fn"), "0x1fn");
}
#[test]
fn regex_flags_sorted_alphabetically() {
use super::sort_regex_flags as srf;
assert_eq!(srf("vg"), "gv");
assert_eq!(srf("yim"), "imy");
assert_eq!(srf("gimsuy"), "gimsuy");
assert_eq!(srf("g"), "g");
assert_eq!(srf(""), "");
}
#[test]
fn valid_js_identifier_accepts_and_rejects() {
use super::is_valid_js_identifier as vid;
assert!(vid("foo"));
assert!(vid("$x"));
assert!(vid("_"));
assert!(vid("a1"));
assert!(vid("camelCase"));
assert!(vid("class"));
assert!(!vid(""));
assert!(!vid("1a"));
assert!(!vid("a-b"));
assert!(!vid("a b"));
assert!(!vid("a.b"));
}
}