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 |
#790 in Web programming
13,910 downloads per month
Used in 2 crates
6MB
140K
SLoC
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
-
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(); } }
-
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;
-
Use the APIs provided by
builders.rs
,formatter
andformat_extensions.rs
.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;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;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()])?; } } }
-
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.
-
Use the
quick_test.rs
file intests/
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/
directoryTypeScript
=>ts/
directoryJSX
=>jsx/
directoryTSX
=>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:
- create a folder called
arrow_with_spaces/
and move the JS file there; - then create a file called
options.json
- The content would be something like:
{ "cases": [ { "line_width": 120, "indent_style": {"Space": 4} } ] }
- the
cases
keyword is mandatory; - 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 andident_style
with 4 spaces - 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
~254K SLoC