6 releases (breaking)

0.5.7 Mar 12, 2024
0.4.0 Jan 9, 2024
0.3.1 Nov 26, 2023
0.1.0 Sep 28, 2023
0.0.2 Sep 28, 2023

#231 in Development tools

Download history 28/week @ 2024-01-05 71/week @ 2024-01-12 20/week @ 2024-01-19 114/week @ 2024-01-26 58/week @ 2024-02-02 2/week @ 2024-02-09 39/week @ 2024-02-16 91/week @ 2024-02-23 89/week @ 2024-03-01 233/week @ 2024-03-08 29/week @ 2024-03-15 3/week @ 2024-03-22 66/week @ 2024-03-29 107/week @ 2024-04-05

216 downloads per month
Used in dprint-plugin-biome

MIT/Apache

6MB
140K SLoC

Biome - Toolchain of the web

Discord chat cargo version

biome_js_formatter

Biome's JavaScript formatter implementation. Follow the documentation.


lib.rs:

Biome's official JavaScript formatter.

Implement the formatter

Our formatter is node based. Meaning that each AST node knows how to format itself. In order to implement the formatting, a node has to implement the trait FormatNode.

biome has an automatic code generation that creates automatically the files out of the grammar. By default, all implementations will format verbatim, meaning that the formatter will print tokens and trivia as they are (format_verbatim).

Our formatter has its own internal IR, it creates its own abstraction from an AST.

The developer won't be creating directly this IR, but they will use a series of utilities that will help to create this IR. The whole IR is represented by the enum FormatElement.

Best Practices

  1. Use the *Fields struct to extract all the tokens/nodes

    #[derive(Debug, Clone, Default)]
    pub struct FormatJsExportDefaultExpressionClause;
    
    impl FormatNodeRule<JsExportDefaultExpressionClause> for FormatJsExportDefaultExpressionClauses {
        fn fmt_fields(&self, node: &JsExportDefaultExpressionClause, f: &mut JsFormatter) -> FormatResult<()> {
            let JsExportDefaultExpressionClauseFields {
                default_token,
                expression,
                semicolon_token,
            }  = node.as_fields();
       }
    }
    
  2. When using .as_fields() with the destructuring, don't use the .. feature. Prefer extracting all fields and ignore them using the _

    #[derive(Debug, Clone, Default)]
    pub struct FormatJsExportDefaultExpressionClause;
    
    impl FormatNodeRule<JsExportDefaultExpressionClause> for FormatJsExportDefaultExpressionClauses {
        fn fmt_fields(&self, node: &JsExportDefaultExpressionClause, f: &mut JsFormatter) -> FormatResult<()> {
             let JsExportDefaultExpressionClauseFields {
                 default_token,
                 expression: _,
                 semicolon_token
             } = node.as_fields();
         }
    }
    

    The reason why we want to promote this pattern is because we want to make explicit when a token/node is excluded;

  3. Use the APIs provided by builders.rs, formatter and format_extensions.rs.

    1. builders.rs exposes a series of utilities to craft the formatter IR; please refer to their internal documentation to understand what the utilities are for;
    2. formatter exposes a set of functions to help to format some recurring patterns; please refer to their internal documentation to understand how to use them and when;
    3. format_extensions.rs: with these traits, we give the ability to nodes and tokens to implements certain methods that are exposed based on its type. If you have a good IDE support, this feature will help you. For example:
    #[derive(Debug, Clone, Default)]
    pub struct FormatJsExportDefaultExpressionClause;
    
    impl FormatNodeRule<JsExportDefaultExpressionClause> for FormatJsExportDefaultExpressionClauses{
         fn fmt_fields(&self, node: &JsExportDefaultExpressionClause, f: &mut JsFormatter) -> FormatResult<()> {
             let JsExportDefaultExpressionClauseFields {
                 default_token,
                 expression, // it's a mandatory node
                 semicolon_token, // this is not a mandatory node
             } = node.as_fields();
             let element = expression.format();
    
             if let Some(expression) = &expression? {
                 write!(f, [expression.format(), space()])?;
             }
    
             if let Some(semicolon) = &semicolon_token {
                 write!(f, [semicolon.format()])?;
             } else {
                 write!(f, [space()])?;
             }
         }
    }
    
  4. Use the playground to inspect the code that you want to format. It helps you to understand which nodes need to be implemented/modified in order to implement formatting. Alternatively, you can locally run the playground by following the playground instructions.

  5. Use the quick_test.rs file in tests/ directory. function to test you snippet straight from your IDE, without running the whole test suite. The test is ignored on purpose, so you won't need to worry about the CI breaking.

Testing

We use insta.rs for our snapshot tests, please make sure you read its documentation to learn the basics of snapshot testing. You should install the companion cargo-insta command to assist with snapshot reviewing.

Directories are divided by language, so when creating a new test file, make sure to have the correct file under the correct folder:

  • JavaScript => js/ directory
  • TypeScript => ts/ directory
  • JSX => jsx/ directory
  • TSX => ts/ directory

To create a new snapshot test for JavaScript, create a new file to crates/biome_js_formatter/tests/specs/js/, e.g. arrow_with_spaces.js

const foo     = ()    => {
    return bar
}

Files processed as modules must go inside the module/ directory, files processed as script must go inside the script/ directory.

Run the following command to generate the new snapshot (the snapshot tests are generated by a procedure macro so we need to recompile the tests):

touch crates/biome_js_formatter/tests/spec_tests.rs && cargo test -p biome_js_formatter formatter

For better test driven development flow, start the formatter tests with cargo-watch:

cargo watch -i '*.new' -x 'test -p biome_js_formatter formatter'

After test execution, you will get a new arrow.js.snap.new file.

To actually update the snapshot, run cargo insta review to interactively review and accept the pending snapshot. arrow.js.snap.new will be replaced with arrow.js.snap

Sometimes, you need to verify the formatting for different cases/options. In order to do that, create a folder with the cases you need to verify. If we needed to follow the previous example:

  1. create a folder called arrow_with_spaces/ and move the JS file there;
  2. then create a file called options.json
  3. The content would be something like:
    {
        "cases": [
            {
                "line_width": 120,
                "indent_style": {"Space": 4}
            }
        ]
    }
    
  4. the cases keyword is mandatory;
  5. then each object of the array will contain the matrix of options you'd want to test. In this case the test suite will run a second test case with line_width to 120 and ident_style with 4 spaces
  6. when the test suite is run, you will have two outputs in your snapshot: the default one and the custom one

Debugging Test Failures

There are four cases when a test is not correct:

  • you try to print/format the same token multiple times; the formatter will check at runtime when a test is run;

  • some tokens haven't been printed; usually you will have this information inside the snapshot, under a section called "Unimplemented tokens/nodes"; a test, in order to be valid, can't have that section;

    If removing a token is the actual behaviour (removing some parenthesis or a semicolon), then the correct way to do it by using the formatter API biome_formatter::trivia::format_removed;

  • the emitted code is not a valid program anymore, the test suite will parse again the emitted code and it will fail if there are syntax errors;

  • the emitted code, when formatted again, differs from the original; this usually happens when removing/adding new elements, and the grouping is not correctly set;

Dependencies

~10–19MB
~222K SLoC