mod cst;
mod js;
mod jsx;
mod prelude;
mod ts;
pub mod utils;
#[rustfmt::skip]
mod generated;
pub mod comments;
pub mod context;
mod parentheses;
pub(crate) mod separated;
mod syntax_rewriter;
use biome_formatter::format_element::tag::Label;
use biome_formatter::prelude::*;
use biome_formatter::{
comments::Comments, write, CstFormatContext, Format, FormatLanguage, FormatToken,
TransformSourceMap,
};
use biome_formatter::{Buffer, FormatOwnedWithRule, FormatRefWithRule, Formatted, Printed};
use biome_js_syntax::{
AnyJsDeclaration, AnyJsStatement, JsLanguage, JsSyntaxKind, JsSyntaxNode, JsSyntaxToken,
};
use biome_rowan::TextRange;
use biome_rowan::{AstNode, SyntaxNode};
use crate::comments::JsCommentStyle;
use crate::context::{JsFormatContext, JsFormatOptions};
use crate::cst::FormatJsSyntaxNode;
use crate::syntax_rewriter::transform;
pub(crate) trait AsFormat<Context> {
type Format<'a>: biome_formatter::Format<Context>
where
Self: 'a;
fn format(&self) -> Self::Format<'_>;
}
impl<T, C> AsFormat<C> for &T
where
T: AsFormat<C>,
{
type Format<'a> = T::Format<'a> where Self: 'a;
fn format(&self) -> Self::Format<'_> {
AsFormat::format(&**self)
}
}
impl<T, C> AsFormat<C> for biome_rowan::SyntaxResult<T>
where
T: AsFormat<C>,
{
type Format<'a> = biome_rowan::SyntaxResult<T::Format<'a>> where Self: 'a;
fn format(&self) -> Self::Format<'_> {
match self {
Ok(value) => Ok(value.format()),
Err(err) => Err(*err),
}
}
}
impl<T, C> AsFormat<C> for Option<T>
where
T: AsFormat<C>,
{
type Format<'a> = Option<T::Format<'a>> where Self: 'a;
fn format(&self) -> Self::Format<'_> {
self.as_ref().map(|value| value.format())
}
}
pub(crate) trait IntoFormat<Context> {
type Format: biome_formatter::Format<Context>;
fn into_format(self) -> Self::Format;
}
impl<T, Context> IntoFormat<Context> for biome_rowan::SyntaxResult<T>
where
T: IntoFormat<Context>,
{
type Format = biome_rowan::SyntaxResult<T::Format>;
fn into_format(self) -> Self::Format {
self.map(IntoFormat::into_format)
}
}
impl<T, Context> IntoFormat<Context> for Option<T>
where
T: IntoFormat<Context>,
{
type Format = Option<T::Format>;
fn into_format(self) -> Self::Format {
self.map(IntoFormat::into_format)
}
}
pub(crate) trait FormattedIterExt {
fn formatted<Context>(self) -> FormattedIter<Self, Self::Item, Context>
where
Self: Iterator + Sized,
Self::Item: IntoFormat<Context>,
{
FormattedIter {
inner: self,
options: std::marker::PhantomData,
}
}
}
impl<I> FormattedIterExt for I where I: std::iter::Iterator {}
pub(crate) struct FormattedIter<Iter, Item, Context>
where
Iter: Iterator<Item = Item>,
{
inner: Iter,
options: std::marker::PhantomData<Context>,
}
impl<Iter, Item, Context> std::iter::Iterator for FormattedIter<Iter, Item, Context>
where
Iter: Iterator<Item = Item>,
Item: IntoFormat<Context>,
{
type Item = Item::Format;
fn next(&mut self) -> Option<Self::Item> {
Some(self.inner.next()?.into_format())
}
}
impl<Iter, Item, Context> std::iter::FusedIterator for FormattedIter<Iter, Item, Context>
where
Iter: std::iter::FusedIterator<Item = Item>,
Item: IntoFormat<Context>,
{
}
impl<Iter, Item, Context> std::iter::ExactSizeIterator for FormattedIter<Iter, Item, Context>
where
Iter: Iterator<Item = Item> + std::iter::ExactSizeIterator,
Item: IntoFormat<Context>,
{
}
pub(crate) type JsFormatter<'buf> = Formatter<'buf, JsFormatContext>;
pub(crate) trait FormatNodeRule<N>
where
N: AstNode<Language = JsLanguage>,
{
fn fmt(&self, node: &N, f: &mut JsFormatter) -> FormatResult<()> {
if self.is_suppressed(node, f) {
return write!(f, [format_suppressed_node(node.syntax())]);
}
self.fmt_leading_comments(node, f)?;
self.fmt_node(node, f)?;
self.fmt_dangling_comments(node, f)?;
self.fmt_trailing_comments(node, f)
}
fn fmt_node(&self, node: &N, f: &mut JsFormatter) -> FormatResult<()> {
let needs_parentheses = self.needs_parentheses(node);
if needs_parentheses {
write!(f, [text("(")])?;
}
self.fmt_fields(node, f)?;
if needs_parentheses {
write!(f, [text(")")])?;
}
Ok(())
}
fn fmt_fields(&self, item: &N, f: &mut JsFormatter) -> FormatResult<()>;
fn needs_parentheses(&self, item: &N) -> bool {
let _ = item;
false
}
fn is_suppressed(&self, node: &N, f: &JsFormatter) -> bool {
f.context().comments().is_suppressed(node.syntax())
}
fn fmt_leading_comments(&self, node: &N, f: &mut JsFormatter) -> FormatResult<()> {
format_leading_comments(node.syntax()).fmt(f)
}
fn fmt_dangling_comments(&self, node: &N, f: &mut JsFormatter) -> FormatResult<()> {
format_dangling_comments(node.syntax())
.with_soft_block_indent()
.fmt(f)
}
fn fmt_trailing_comments(&self, node: &N, f: &mut JsFormatter) -> FormatResult<()> {
format_trailing_comments(node.syntax()).fmt(f)
}
}
pub(crate) trait FormatBogusNodeRule<N>
where
N: AstNode<Language = JsLanguage>,
{
fn fmt(&self, node: &N, f: &mut JsFormatter) -> FormatResult<()> {
format_bogus_node(node.syntax()).fmt(f)
}
}
pub(crate) type FormatJsSyntaxToken = FormatToken<JsFormatContext>;
impl AsFormat<JsFormatContext> for JsSyntaxToken {
type Format<'a> = FormatRefWithRule<'a, JsSyntaxToken, FormatJsSyntaxToken>;
fn format(&self) -> Self::Format<'_> {
FormatRefWithRule::new(self, FormatJsSyntaxToken::default())
}
}
impl IntoFormat<JsFormatContext> for JsSyntaxToken {
type Format = FormatOwnedWithRule<JsSyntaxToken, FormatJsSyntaxToken>;
fn into_format(self) -> Self::Format {
FormatOwnedWithRule::new(self, FormatJsSyntaxToken::default())
}
}
#[derive(Debug, Clone)]
pub struct JsFormatLanguage {
options: JsFormatOptions,
}
impl JsFormatLanguage {
pub fn new(options: JsFormatOptions) -> Self {
Self { options }
}
}
impl FormatLanguage for JsFormatLanguage {
type SyntaxLanguage = JsLanguage;
type Context = JsFormatContext;
type FormatRule = FormatJsSyntaxNode;
fn transform(
&self,
root: &SyntaxNode<Self::SyntaxLanguage>,
) -> Option<(SyntaxNode<Self::SyntaxLanguage>, TransformSourceMap)> {
Some(transform(root.clone()))
}
fn is_range_formatting_node(&self, node: &JsSyntaxNode) -> bool {
let kind = node.kind();
if matches!(kind, JsSyntaxKind::JS_VARIABLE_DECLARATION) {
return false;
}
AnyJsStatement::can_cast(kind)
|| AnyJsDeclaration::can_cast(kind)
|| matches!(
kind,
JsSyntaxKind::JS_DIRECTIVE | JsSyntaxKind::JS_EXPORT | JsSyntaxKind::JS_IMPORT
)
}
fn options(&self) -> &JsFormatOptions {
&self.options
}
fn create_context(
self,
root: &JsSyntaxNode,
source_map: Option<TransformSourceMap>,
) -> Self::Context {
let comments = Comments::from_node(root, &JsCommentStyle, source_map.as_ref());
JsFormatContext::new(self.options, comments).with_source_map(source_map)
}
}
pub fn format_range(
options: JsFormatOptions,
root: &JsSyntaxNode,
range: TextRange,
) -> FormatResult<Printed> {
biome_formatter::format_range(root, range, JsFormatLanguage::new(options))
}
pub fn format_node(
options: JsFormatOptions,
root: &JsSyntaxNode,
) -> FormatResult<Formatted<JsFormatContext>> {
biome_formatter::format_node(root, JsFormatLanguage::new(options))
}
pub fn format_sub_tree(options: JsFormatOptions, root: &JsSyntaxNode) -> FormatResult<Printed> {
biome_formatter::format_sub_tree(root, JsFormatLanguage::new(options))
}
#[derive(Copy, Clone, Debug)]
pub(crate) enum JsLabels {
MemberChain,
}
impl Label for JsLabels {
fn id(&self) -> u64 {
*self as u64
}
fn debug_name(&self) -> &'static str {
match self {
JsLabels::MemberChain => "MemberChain",
}
}
}
#[cfg(test)]
mod tests {
use super::format_range;
use crate::context::JsFormatOptions;
use biome_formatter::IndentStyle;
use biome_js_parser::{parse, parse_script, JsParserOptions};
use biome_js_syntax::JsFileSource;
use biome_rowan::{TextRange, TextSize};
#[test]
fn test_range_formatting() {
let input = "
while(
true
) {
function func() {
func( /* comment */
);
let array =
[ 1
, 2];
}
function func2()
{
const no_format = () => {};
}
}
";
let range_start = TextSize::try_from(input.find("let").unwrap() - 2).unwrap();
let range_end = TextSize::try_from(input.find("const").unwrap()).unwrap();
let tree = parse_script(input, JsParserOptions::default());
let result = format_range(
JsFormatOptions::new(JsFileSource::js_script())
.with_indent_style(IndentStyle::Space)
.with_indent_width(4.into()),
&tree.syntax(),
TextRange::new(range_start, range_end),
);
let result = result.expect("range formatting failed");
assert_eq!(
result.as_code(),
"function func() {\n func(/* comment */);\n\n let array = [1, 2];\n }\n\n function func2() {\n const no_format = () => {};\n }"
);
assert_eq!(
result.range(),
Some(TextRange::new(
range_start - TextSize::from(56),
range_end + TextSize::from(40)
))
);
}
#[test]
fn test_range_formatting_indentation() {
let input = "
function() {
const veryLongIdentifierToCauseALineBreak = { veryLongKeyToCauseALineBreak: 'veryLongValueToCauseALineBreak' }
}
";
let range_start = TextSize::try_from(input.find("const").unwrap()).unwrap();
let range_end = TextSize::try_from(input.find('}').unwrap()).unwrap();
let tree = parse_script(input, JsParserOptions::default());
let result = format_range(
JsFormatOptions::new(JsFileSource::js_script())
.with_indent_style(IndentStyle::Space)
.with_indent_width(4.into()),
&tree.syntax(),
TextRange::new(range_start, range_end),
);
let result = result.expect("range formatting failed");
assert_eq!(
result.as_code(),
"const veryLongIdentifierToCauseALineBreak = {\n veryLongKeyToCauseALineBreak: \"veryLongValueToCauseALineBreak\",\n };"
);
assert_eq!(
result.range(),
Some(TextRange::new(range_start, range_end + TextSize::from(1)))
);
}
#[test]
fn test_range_formatting_whitespace() {
let input = " ";
let range_start = TextSize::from(5);
let range_end = TextSize::from(5);
let tree = parse_script(input, JsParserOptions::default());
let result = format_range(
JsFormatOptions::new(JsFileSource::js_script())
.with_indent_style(IndentStyle::Space)
.with_indent_width(4.into()),
&tree.syntax(),
TextRange::new(range_start, range_end),
);
let result = result.expect("range formatting failed");
assert_eq!(result.as_code(), "");
assert_eq!(result.range(), Some(TextRange::new(range_start, range_end)));
}
#[test]
fn test_range_formatting_middle_of_token() {
let input = r#"/* */ function Foo(){
/**/
}
"#;
let range = TextRange::new(TextSize::from(16), TextSize::from(28));
debug_assert_eq!(
&input[range],
r#"oo(){
/**/
}"#
);
let tree = parse_script(input, JsParserOptions::default());
let result = format_range(
JsFormatOptions::new(JsFileSource::js_script())
.with_indent_style(IndentStyle::Space)
.with_indent_width(4.into()),
&tree.syntax(),
range,
)
.expect("Range formatting failed");
assert_eq!(
result.as_code(),
r#"/* */ function Foo() {
/**/
}"#
);
assert_eq!(
result.range(),
Some(TextRange::new(TextSize::from(0), TextSize::from(28)))
)
}
#[test]
fn range_formatting_trailing_comments() {
let input = r#"let fn =a((x ) => {
quux (); //
});
"#;
let range = TextRange::new(TextSize::from(28), TextSize::from(41));
debug_assert_eq!(&input[range], r#" quux (); //"#);
let tree = parse_script(input, JsParserOptions::default());
let result = format_range(
JsFormatOptions::new(JsFileSource::js_script())
.with_indent_style(IndentStyle::Space)
.with_indent_width(4.into()),
&tree.syntax(),
range,
)
.expect("Range formatting failed");
assert_eq!(result.as_code(), r#"quux(); //"#);
assert_eq!(
result.range(),
Some(TextRange::new(TextSize::from(30), TextSize::from(41)))
)
}
#[test]
fn format_range_out_of_bounds() {
let src = "statement();";
let syntax = JsFileSource::js_module();
let tree = parse(src, syntax, JsParserOptions::default());
let result = format_range(
JsFormatOptions::new(syntax),
&tree.syntax(),
TextRange::new(TextSize::from(0), TextSize::of(src) + TextSize::from(5)),
);
assert!(result.is_err());
}
}