ruff/crates/ruff_python_formatter/src/format/arguments.rs
Charlie Marsh ca49b00e55
Add initial formatter implementation (#2883)
# Summary

This PR contains the code for the autoformatter proof-of-concept.

## Crate structure

The primary formatting hook is the `fmt` function in `crates/ruff_python_formatter/src/lib.rs`.

The current formatter approach is outlined in `crates/ruff_python_formatter/src/lib.rs`, and is structured as follows:

- Tokenize the code using the RustPython lexer.
- In `crates/ruff_python_formatter/src/trivia.rs`, extract a variety of trivia tokens from the token stream. These include comments, trailing commas, and empty lines.
- Generate the AST via the RustPython parser.
- In `crates/ruff_python_formatter/src/cst.rs`, convert the AST to a CST structure. As of now, the CST is nearly identical to the AST, except that every node gets a `trivia` vector. But we might want to modify it further.
- In `crates/ruff_python_formatter/src/attachment.rs`, attach each trivia token to the corresponding CST node. The logic for this is mostly in `decorate_trivia` and is ported almost directly from Prettier (given each token, find its preceding, following, and enclosing nodes, then attach the token to the appropriate node in a second pass).
- In `crates/ruff_python_formatter/src/newlines.rs`, normalize newlines to match Black’s preferences. This involves traversing the CST and inserting or removing `TriviaToken` values as we go.
- Call `format!` on the CST, which delegates to type-specific formatter implementations (e.g., `crates/ruff_python_formatter/src/format/stmt.rs` for `Stmt` nodes, and similar for `Expr` nodes; the others are trivial). Those type-specific implementations delegate to kind-specific functions (e.g., `format_func_def`).

## Testing and iteration

The formatter is being developed against the Black test suite, which was copied over in-full to `crates/ruff_python_formatter/resources/test/fixtures/black`.

The Black fixtures had to be modified to create `[insta](https://github.com/mitsuhiko/insta)`-compatible snapshots, which now exist in the repo.

My approach thus far has been to try and improve coverage by tackling fixtures one-by-one.

## What works, and what doesn’t

- *Most* nodes are supported at a basic level (though there are a few stragglers at time of writing, like `StmtKind::Try`).
- Newlines are properly preserved in most cases.
- Magic trailing commas are properly preserved in some (but not all) cases.
- Trivial leading and trailing standalone comments mostly work (although maybe not at the end of a file).
- Inline comments, and comments within expressions, often don’t work -- they work in a few cases, but it’s one-off right now. (We’re probably associating them with the “right” nodes more often than we are actually rendering them in the right place.)
- We don’t properly normalize string quotes. (At present, we just repeat any constants verbatim.)
- We’re mishandling a bunch of wrapping cases (if we treat Black as the reference implementation). Here are a few examples (demonstrating Black's stable behavior):

```py
# In some cases, if the end expression is "self-closing" (functions,
# lists, dictionaries, sets, subscript accesses, and any length-two
# boolean operations that end in these elments), Black
# will wrap like this...
if some_expression and f(
    b,
    c,
    d,
):
    pass

# ...whereas we do this:
if (
    some_expression
    and f(
        b,
        c,
        d,
    )
):
    pass

# If function arguments can fit on a single line, then Black will
# format them like this, rather than exploding them vertically.
if f(
    a, b, c, d, e, f, g, ...
):
    pass
```

- We don’t properly preserve parentheses in all cases. Black preserves parentheses in some but not all cases.
2023-02-15 04:06:35 +00:00

123 lines
4 KiB
Rust

use ruff_formatter::prelude::*;
use ruff_formatter::{format_args, write, Format};
use crate::context::ASTFormatContext;
use crate::cst::Arguments;
use crate::shared_traits::AsFormat;
pub struct FormatArguments<'a> {
item: &'a Arguments,
}
impl AsFormat<ASTFormatContext<'_>> for Arguments {
type Format<'a> = FormatArguments<'a>;
fn format(&self) -> Self::Format<'_> {
FormatArguments { item: self }
}
}
impl Format<ASTFormatContext<'_>> for FormatArguments<'_> {
fn fmt(&self, f: &mut Formatter<ASTFormatContext<'_>>) -> FormatResult<()> {
let args = self.item;
let mut first = true;
let defaults_start = args.posonlyargs.len() + args.args.len() - args.defaults.len();
for (i, arg) in args.posonlyargs.iter().chain(&args.args).enumerate() {
if !std::mem::take(&mut first) {
write!(f, [text(",")])?;
write!(f, [soft_line_break_or_space()])?;
}
write!(
f,
[group(&format_args![format_with(|f| {
write!(f, [arg.format()])?;
if let Some(i) = i.checked_sub(defaults_start) {
if arg.node.annotation.is_some() {
write!(f, [space()])?;
write!(f, [text("=")])?;
write!(f, [space()])?;
} else {
write!(f, [text("=")])?;
}
write!(f, [args.defaults[i].format()])?;
}
Ok(())
})])]
)?;
if i + 1 == args.posonlyargs.len() {
if !std::mem::take(&mut first) {
write!(f, [text(",")])?;
write!(f, [soft_line_break_or_space()])?;
}
write!(f, [text("/")])?;
}
}
if let Some(vararg) = &args.vararg {
if !std::mem::take(&mut first) {
write!(f, [text(",")])?;
write!(f, [soft_line_break_or_space()])?;
}
first = false;
write!(f, [text("*")])?;
write!(f, [vararg.format()])?;
} else if !args.kwonlyargs.is_empty() {
if !std::mem::take(&mut first) {
write!(f, [text(",")])?;
write!(f, [soft_line_break_or_space()])?;
}
first = false;
write!(f, [text("*")])?;
}
let defaults_start = args.kwonlyargs.len() - args.kw_defaults.len();
for (i, kwarg) in args.kwonlyargs.iter().enumerate() {
if !std::mem::take(&mut first) {
write!(f, [text(",")])?;
write!(f, [soft_line_break_or_space()])?;
}
write!(
f,
[group(&format_args![format_with(|f| {
write!(f, [kwarg.format()])?;
if let Some(default) = i
.checked_sub(defaults_start)
.and_then(|i| args.kw_defaults.get(i))
{
if kwarg.node.annotation.is_some() {
write!(f, [space()])?;
write!(f, [text("=")])?;
write!(f, [space()])?;
} else {
write!(f, [text("=")])?;
}
write!(f, [default.format()])?;
}
Ok(())
})])]
)?;
}
if let Some(kwarg) = &args.kwarg {
if !std::mem::take(&mut first) {
write!(f, [text(",")])?;
write!(f, [soft_line_break_or_space()])?;
}
write!(f, [text("**")])?;
write!(f, [kwarg.format()])?;
}
if !first {
write!(f, [if_group_breaks(&text(","))])?;
}
Ok(())
}
}